Beispiel #1
0
    def __makeMasterMask(self, I1, I2, poissonSolver=None):
        """

        Calculate the common mask of defocal images.

        Arguments:
            I1 {[Image]} -- Intra- or extra-focal image.
            I2 {[Image]} -- Intra- or extra-focal image.

        Keyword Arguments:
            poissonSolver {[string]} -- Algorithm to solve the Poisson's equation. If the "fft" is
                                        used, the mask dimension will be extended to the order of 2
                                        for the "fft" to use.
        """

        # Get the overlap region of mask for intra- and extra-focal images. This is to avoid the
        # anormalous signal due to difference in vignetting.
        self.pMask = I1.pMask * I2.pMask
        self.cMask = I1.cMask * I2.cMask

        # Change the dimension of image for fft to use
        if (poissonSolver == "fft"):
            padDim = self.parameter["padDim"]
            self.pMaskPad = padArray(self.pMask, padDim)
            self.cMaskPad = padArray(self.cMask, padDim)
Beispiel #2
0
    def _makeMasterMask(self, I1, I2, poissonSolver=None):
        """Calculate the common mask of defocal images.

        Parameters
        ----------
        I1 : CompensableImage
            Intra- or extra-focal image.
        I2 : CompensableImage
            Intra- or extra-focal image.
        poissonSolver : str, optional
            Algorithm to solve the Poisson's equation. If the "fft" is used,
            the mask dimension will be extended to the order of 2 for the "fft"
            to use. (the default is None.)
        """

        # Get the overlap region of mask for intra- and extra-focal images.
        # This is to avoid the anormalous signal due to difference in
        # vignetting.
        self.pMask = I1.getPaddedMask() * I2.getPaddedMask()
        self.cMask = I1.getNonPaddedMask() * I2.getNonPaddedMask()

        # Change the dimension of image for fft to use
        if poissonSolver == "fft":
            padDim = self.getFftDimension()
            self.pMaskPad = padArray(self.pMask, padDim)
            self.cMaskPad = padArray(self.cMask, padDim)
Beispiel #3
0
    def _makeMasterMask(self, I1, I2, poissonSolver=None):
        """Calculate the common mask of defocal images.

        Parameters
        ----------
        I1 : Image
            Intra- or extra-focal image.
        I2 : Image
            Intra- or extra-focal image.
        poissonSolver : str, optional
            Algorithm to solve the Poisson's equation. If the "fft" is used,
            the mask dimension will be extended to the order of 2 for the "fft"
            to use. (the default is None.)
        """

        # Get the overlap region of mask for intra- and extra-focal images.
        # This is to avoid the anormalous signal due to difference in
        # vignetting.
        self.pMask = I1.getPaddedMask() * I2.getPaddedMask()
        self.cMask = I1.getNonPaddedMask() * I2.getNonPaddedMask()

        # Change the dimension of image for fft to use
        if (poissonSolver == "fft"):
            padDim = self.getFftDimension()
            self.pMaskPad = padArray(self.pMask, padDim)
            self.cMaskPad = padArray(self.cMask, padDim)
Beispiel #4
0
def createMTFatm(D, m, k, wlum, zen, r0inmRef, model="vonK"):
    """
    
    Generate the modulation transfer function (MTF) for atmosphere.
    
    Arguments:
        D {[float]} -- Side length of optical path difference (OPD) image in m.
        m {[int]} -- Dimension of OPD image in pixel. The the number of pixel we want to have 
                     to cover the length of D.
        k {[int]} -- Use a k-times bigger array to pad the MTF. Use k=1 for the same size.
        wlum {[float]} -- Wavelength in um.
        zen {[float]} -- Telescope zenith angle in degree.
        r0inmRef {[float]} -- Reference r0 in meter at the wavelength of 0.5 um.
    
    Keyword Arguments:
        model {str} -- Kolmogorov power spectrum ("Kolm") or van Karman power spectrum ("vonK"). 
                       (default: {"vonK"})
    
    Returns:
        [ndarray] -- MTF at specific atmosphere model.
    """

    # Get the atmosphere phase structure function
    sfa = atmSF(D, m, wlum, zen, r0inmRef, model)

    # Get the modular transfer function for atmosphere
    mtfa = np.exp(-0.5 * sfa)

    # Add even number
    N = int(m + np.rint((m * (k - 1) + 1e-5) / 2) * 2)

    # Pad the matrix if necessary
    mtfa = padArray(mtfa, N)

    return mtfa
Beispiel #5
0
    def __createSignal(self, inst, I1, I2, cliplevel):
        """

        Calculate the wavefront singal for "fft" to use in solving the Poisson's equation.

        Need to discuss the method to define threshold and discuss to use np.median() instead.
        Need to discuss why the calculation of I0 is different from "exp".

        Arguments:
            inst {[Instrument]} -- Instrument to use.
            I1 {[Image]} -- Intra- or extra-focal image.
            I2 {[Image]} -- Intra- or extra-focal image.
            cliplevel {[float]} -- Parameter to determine the threshold of calculating I0.

        Returns:
            [float] -- Approximated wavefront signal.
        """

        # Check the condition of images
        I1image, I2image = self.__checkImageDim(I1, I2)

        # Wavefront signal S=-(1/I0)*(dI/dz) is approximated to be -(1/delta z)*(I1-I2)/(I1+I2)
        num = I1image - I2image
        den = I1image + I2image

        # Define the effective minimum central signal element by the threshold ( I0=(I1+I2)/2 )

        # Calculate the threshold
        pixelList = den * self.cMask
        pixelList = pixelList[pixelList != 0]

        low = pixelList.min()
        high = pixelList.max()
        medianThreshold = (high - low) / 2. + low

        # Define the effective minimum central signal element
        den[den < medianThreshold * cliplevel] = 1.5 * medianThreshold

        # Calculate delta z = f(f-l)/l, f: focal length, l: defocus distance of the image planes
        focalLength = inst.parameter["focalLength"]
        offset = inst.parameter["offset"]
        deltaZ = focalLength * (focalLength - offset) / offset

        # Calculate the wavefront signal. Enforce the element outside the mask to be 0.
        den[den == 0] = np.inf

        # Calculate the wavefront signal
        S = num / den / deltaZ

        # Extend the dimension of signal to the order of 2 for "fft" to use
        padDim = self.parameter["padDim"]
        Sout = padArray(S, padDim) * self.cMaskPad

        return Sout
Beispiel #6
0
def createMTFatm(D, m, k, wlum, zen, r0inmRef, model="vonK"):
    """Generate the modulation transfer function (MTF) for atmosphere.

    Parameters
    ----------
    D : float
        Side length of optical path difference (OPD) image in m.
    m : int
        Dimension of OPD image in pixel. The the number of pixel we want to
        have to cover the length of D.
    k : int
        Use a k-times bigger array to pad the MTF. Use k=1 for the same size.
    wlum : float
        Wavelength in um.
    zen : float
        Telescope zenith angle in degree.
    r0inmRef : float
        Reference r0 in meter at the wavelength of 0.5 um.
    model : str, optional
        Kolmogorov power spectrum ("Kolm") or van Karman power spectrum
        ("vonK"). (the default is "vonK".)

    Returns
    -------
    numpy.ndarray
        MTF at specific atmosphere model.
    """

    # Get the atmosphere phase structure function
    sfa = atmSF(D, m, wlum, zen, r0inmRef, model)

    # Get the modular transfer function for atmosphere
    mtfa = np.exp(-0.5 * sfa)

    # Add even number
    N = int(m + np.rint((m * (k - 1) + 1e-5) / 2) * 2)

    # Pad the matrix if necessary
    mtfa = padArray(mtfa, N)

    return mtfa
Beispiel #7
0
def calc_pssn(array,
              wlum,
              aType="opd",
              D=8.36,
              r0inmRef=0.1382,
              zen=0,
              pmask=0,
              imagedelta=0,
              fno=1.2335,
              debugLevel=0):
    """
    
    Calculate the normalized point source sensitivity (PSSN).
    
    Arguments:
        array {[ndarray]} -- Array that contains either opd or pdf. opd need to be in microns.
        wlum {[float]} -- Wavelength in microns.
    
    Keyword Arguments:
        aType {str} -- What is used to calculate pssn - either opd or psf. (default: {"opd"})
        D {float} -- Side length of OPD image in meter. (default: {8.36})
        r0inmRef {float} -- Fidicial atmosphere r0 @ 500nm in meter, Konstantinos uses 0.20. 
                            (default: {0.1382})
        zen {float} -- Telescope zenith angle in degree. (default: {0})
        pmask {float/ ndarray} -- Pupil mask. when opd is used, it can be generated using opd 
                                  image, we can put 0 or -1 or whatever here. When psf is used, 
                                  this needs to be provided separately with same size as array. 
                                  (default: {0})
        imagedelta {float} -- Only needed when psf is used. use 0 for opd. (default: {0})
        fno {float} -- Only needed when psf is used. use 0 for opd. (default: {1.2335})
        debugLevel {int} -- Debug level. The higher value gives more information. (default: {0})
    
    Returns:
        [float] -- PSSN value.
    """

    # Only needed for psf: pmask, imagedelta, fno

    # THE INTERNAL RESOLUTION THAT FFTS OPERATE ON IS VERY IMPORTANT
    # TO THE ACCUARCY OF PSSN.
    # WHEN TYPE='OPD', NRESO=SIZE(ARRAY,1)
    # WHEN TYPE='PSF', NRESO=SIZE(PMASK,1)
    #    for the psf option, we can not first convert psf back to opd then
    #    start over,
    #    because psf=|exp(-2*OPD)|^2. information has been lost in the | |^2.
    #    we need to go forward with psf->mtf,
    #    and take care of the coordinates properly.

    # PSSN = (n_eff)_atm / (n_eff)_atm+sys
    # (n_eff))_atm = 1 / (int (PSF^2)_atm dOmega)
    # (n_eff))_atm+sys = 1 / (int (PSF^2)_atm+sys dOmega)

    # Check the type is "OPD" or "PSF"
    if aType not in ("opd", "psf"):
        raise ValueError("The type of %s is not allowed." % aType)

    # Squeeze the array if necessary
    if (array.ndim == 3):
        array2D = array[0, :, :].squeeze()

    # Get the k value (magnification ratio used in creating MTF)
    if (aType == "opd"):
        try:
            m = max(array2D.shape)
        except NameError:
            m = max(array.shape)
        k = 1
    elif (aType == "psf"):
        m = max(pmask.shape)
        # Pupil needs to be padded k times larger to get imagedelta
        # Do not know where to find this formular. Check with Bo.
        k = fno * wlum / imagedelta

    # Get the modulation transfer function with the van Karman power spectrum
    mtfa = createMTFatm(D, m, k, wlum, zen, r0inmRef, model="vonK")

    # Get the pupil function
    if (aType == "opd"):
        try:
            iad = (array2D != 0)
        except NameError:
            iad = (array != 0)
    elif (aType == "psf"):
        # Add even number
        mk = int(m + np.rint((m * (k - 1) + 1e-5) / 2) * 2)
        # padArray(pmask, m)
        iad = pmask

    # OPD --> PSF --> OTF --> OTF' (OTF + atmosphere) --> PSF'
    # Check with Bo that we could get OTF' or PSF' from PhoSim or not directly.
    # The above question might not be a concern in the simulation.
    # However, for the real image, it loooks like this is hard to do
    # What should be the standard way to judge the PSSN in the real telescope?

    # OPD is zero for perfect telescope
    opdt = np.zeros((m, m))

    # OPD to PSF
    psft = opd2psf(opdt,
                   iad,
                   wlum,
                   imagedelta=imagedelta,
                   sensorFactor=1,
                   fno=fno,
                   debugLevel=debugLevel)

    # PSF to optical transfer function (OTF)
    otft = psf2otf(psft)

    # Add atmosphere to perfect telescope
    otfa = otft * mtfa

    # OTF to PSF
    psfa = otf2psf(otfa)

    # Atmospheric PSS (point spread sensitivity) = 1/neff_atm
    pssa = np.sum(psfa**2)

    # Calculate PSF with error (atmosphere + system)
    if (aType == "opd"):

        if (array.ndim == 2):
            ninst = 1
        else:
            ninst = array.shape[0]

        for ii in range(ninst):

            if (array.ndim == 2):
                array2D = array
            else:
                array2D = array[ii, :, :].squeeze()

            psfei = opd2psf(array2D, iad, wlum, debugLevel=debugLevel)

            if (ii == 0):
                psfe = psfei
            else:
                psfe += psfei

        # Do the normalization based on the number of instrument
        psfe = psfe / ninst

    elif (aType == "psf"):

        if (array.shape[0] == mk):
            psfe = array

        elif (array.shape[0] > mk):
            psfe = extractArray(array, mk)

        else:
            print("calc_pssn: image provided too small, %d < %d x %6.4f." %
                  (array.shape[0], m, k))
            print("IQ is over-estimated !!!")
            psfe = padArray(array, mk)

        # Do the normalization of PSF
        psfe = psfe / np.sum(psfe) * np.sum(psft)

    # OTF with system error
    otfe = psf2otf(psfe)

    # Add the atmosphere error
    # OTF with system and atmosphere errors
    otftot = otfe * mtfa

    # PSF with system and atmosphere errors
    psftot = otf2psf(otftot)

    # atmospheric + error PSS
    pss = np.sum(psftot**2)

    # normalized PSS
    pssn = pss / pssa

    if (debugLevel >= 3):
        print("pssn = %10.8e/%10.8e = %6.4f." % (pss, pssa, pssn))

    return pssn
Beispiel #8
0
def opd2psf(opd,
            pupil,
            wavelength,
            imagedelta=0,
            sensorFactor=1,
            fno=1.2335,
            debugLevel=0):
    """
    
    Optical path difference (OPD) to point spread function (PSF).
    
    Arguments:
        opd {[ndarray]} -- Optical path difference.
        pupil {[ndarray/ float/ int]} -- Pupil function. If pupil is a number, not an array, we will 
                                         get pupil geometry from OPD.
        wavelength {[float]} -- Wavelength in um.
    
    Keyword Arguments:
        imagedelta {float} -- Pixel size in um. Use 0 if pixel size is not specified. (default: {0})
        sensorFactor {float} -- Factor of sensor (check with Bo for this). Only need this if 
                                imagedelta != 0. (default: {1})
        fno {float} -- ? Check with Bo. Only need this if imagedelta=0. (default: {1.2335})
        debugLevel {int} -- Debug level. The higher value gives more information. (default: {0})
    
    Returns:
        [ndarray] -- Normalized PSF.
    
    Raises:
        ValueError -- Shapes of OPD and pupil are different.
        ValueError -- OPD shape is not square.
        ValueError -- Padding value is less than 1.
    """

    # Make sure all NaN in OPD to be 0
    opd[np.isnan(opd)] = 0

    # Get the pupil function from OPD if necessary
    if (not isinstance(pupil, np.ndarray)):
        pupil = (opd != 0)

    # Check the dimension of pupil and OPD should be the same
    if (opd.shape != pupil.shape):
        raise ValueError("Shapes of OPD and pupil are different.")

    # For the PSF
    if (imagedelta != 0):

        # Check the dimension of OPD
        if (opd.shape[0] != opd.shape[1]):
            raise ValueError("Error (opd2psf): OPD image size = (%d, %d)." %
                             (opd.shape[0], opd.shape[1]))

        # Get the k value and the padding
        k = fno * wavelength / imagedelta
        padding = k / sensorFactor

        # Check the padding
        if (padding < 1):

            errorMes = "opd2psf: Sampling too low, data inaccurate.\n"
            errorMes += "Imagedelta needs to be smaller than fno * wlum = %4.2f um.\n" % (
                fno * wavelength)
            errorMes += "So that the padding factor > 1.\n"
            errorMes += "Otherwise we have to cut pupil to be < D."

            raise ValueError(errorMes)

        # Size of sensor
        sensorSamples = opd.shape[0]

        # Add even number for padding
        N = int(sensorSamples +
                np.rint(((padding - 1) * sensorSamples + 1e-5) / 2) * 2)
        pupil = padArray(pupil, N)
        opd = padArray(opd, N)

        # Show the padding information or not
        if (debugLevel >= 3):
            print("padding = %8.6f." % padding)

    # If imagedelta = 0, we don't do any padding, and go with below
    z = pupil * np.exp(-2j * np.pi * opd / wavelength)
    z = np.fft.fftshift(np.fft.fft2(np.fft.fftshift(z), s=z.shape))
    z = np.absolute(z**2)

    # Normalize the PSF
    z = z / np.sum(z)

    # Show the information of PSF from OPD
    if (debugLevel >= 3):
        print("opd2psf(): imagedelta = %8.6f." % imagedelta, end="")

        if (imagedelta == 0):
            print("0 means using OPD with padding as provided.")

        print("Verify psf has been normalized: %4.1f." % np.sum(z))

    return z
Beispiel #9
0
    def testFunc(self):

        # Get the path of module
        modulePath = getModulePath()

        # Obscuration
        e = 0

        # Calculate the radius
        dd = np.sqrt(self.xx**2 + self.yy**2)

        # Define the invalid range
        idx = (dd > 1) | (dd < e)

        # Create the Zernike terms
        Z = np.zeros(22)

        # Generate the map of Z12
        Z[11] = 1

        # Calculate the map of Zernike polynomial
        Zmap = ZernikeAnnularEval(Z, self.xx, self.yy, e)
        Zmap[idx] = np.nan

        # Put the elements to be 0 in the invalid region
        Zmap[np.isnan(Zmap)] = 0

        # Check the normalization for Z1 - Z28
        e = 0.61
        ansValue = np.pi * (1 - e**2)
        for ii in range(28):
            Z = np.zeros(28)
            Z[ii] = 1
            funcNor = lambda r, theta: r * ZernikeAnnularEval(
                Z, r * np.cos(theta), r * np.sin(theta), e)**2
            normalization = nquad(funcNor, [[e, 1], [0, 2 * np.pi]])[0]
            self.assertAlmostEqual(normalization, ansValue)

        # Check the orthogonality for Z1 - Z28
        for jj in range(28):
            Z1 = np.zeros(28)
            Z1[jj] = 1
            for ii in range(28):
                if (ii != jj):
                    Z2 = np.zeros(28)
                    Z2[ii] = 1
                    funcOrtho = lambda r, theta: r*ZernikeAnnularEval(Z1, r*np.cos(theta), r*np.sin(theta), e) * \
                                                   ZernikeAnnularEval(Z2, r*np.cos(theta), r*np.sin(theta), e)
                    orthogonality = nquad(funcOrtho,
                                          [[e, 1], [0, 2 * np.pi]])[0]
                    self.assertAlmostEqual(orthogonality, 0)

        # Increase the dimension
        ZmapInc = padArray(Zmap, Zmap.shape[0] + 20)
        self.assertAlmostEqual(ZmapInc.shape[0], Zmap.shape[0] + 20)

        # Decrease the dimension
        ZmapDec = extractArray(ZmapInc, Zmap.shape[0])
        self.assertAlmostEqual(ZmapDec.shape[0], Zmap.shape[0])

        # Test the reading of file
        configFilePath = os.path.join(modulePath, "configData", "cwfs",
                                      "instruData", "lsst", "lsst.param")
        varName = "Focal_length"
        value = getConfigValue(configFilePath, varName, index=2)
        self.assertEqual(value, 10.312)
Beispiel #10
0
    def _createSignal(self, I1, I2, cliplevel):
        """Calculate the wavefront singal for "fft" to use in solving the
        Poisson's equation.

        Need to discuss the method to define threshold and discuss to use
        np.median() instead.
        Need to discuss why the calculation of I0 is different from "exp".

        Parameters
        ----------
        I1 : Image
            Intra- or extra-focal image.
        I2 : Image
            Intra- or extra-focal image.
        cliplevel : float
            Parameter to determine the threshold of calculating I0.

        Returns
        -------
        numpy.ndarray
            Approximated wavefront signal.
        """

        # Check the condition of images
        I1image, I2image = self._checkImageDim(I1, I2)

        # Wavefront signal S=-(1/I0)*(dI/dz) is approximated to be
        # -(1/delta z)*(I1-I2)/(I1+I2)
        num = I1image - I2image
        den = I1image + I2image

        # Define the effective minimum central signal element by the threshold
        # ( I0=(I1+I2)/2 )

        # Calculate the threshold
        pixelList = den * self.cMask
        pixelList = pixelList[pixelList != 0]

        low = pixelList.min()
        high = pixelList.max()
        medianThreshold = (high-low)/2. + low

        # Define the effective minimum central signal element
        den[den < medianThreshold*cliplevel] = 1.5*medianThreshold

        # Calculate delta z = f(f-l)/l, f: focal length, l: defocus distance of
        # the image planes
        focalLength = self._inst.getFocalLength()
        offset = self._inst.getDefocalDisOffset()
        deltaZ = focalLength*(focalLength-offset)/offset

        # Calculate the wavefront signal. Enforce the element outside the mask
        # to be 0.
        den[den == 0] = np.inf

        # Calculate the wavefront signal
        S = num/den/deltaZ

        # Extend the dimension of signal to the order of 2 for "fft" to use
        padDim = self.getFftDimension()
        Sout = padArray(S, padDim)*self.cMaskPad

        return Sout
Beispiel #11
0
    def compensate(self, inst, algo, zcCol, model):
        """Calculate the image compensated from the affection of wavefront.

        Parameters
        ----------
        inst : Instrument
            Instrument to use.
        algo : Algorithm
            Algorithm to solve the Poisson's equation. It can by done by the
            fast Fourier transform or serial expansion.
        zcCol : numpy.ndarray
            Coefficients of wavefront.
        model : str
            Optical model. It can be "paraxial", "onAxis", or "offAxis".

        Raises
        ------
        RuntimeError
            input:size zcCol in compensate needs to be a numTerms row column
            vector.
        """

        # Check the condition of inputs
        numTerms = algo.getNumOfZernikes()
        if ((zcCol.ndim == 1) and (len(zcCol) != numTerms)):
            raise RuntimeError("input:size",
                               "zcCol in compensate needs to be a %d row column vector. \n" % numTerms)

        # Dimension of image
        sm, sn = self._image.getImg().shape

        # Dimenstion of projected image on focal plane
        projSamples = sm

        # Let us create a look-up table for x -> xp first.
        luty, lutx = np.mgrid[-(projSamples/2 - 0.5):(projSamples/2 + 0.5),
                              -(projSamples/2 - 0.5):(projSamples/2 + 0.5)]

        sensorFactor = inst.getSensorFactor()
        lutx = lutx/(projSamples/2/sensorFactor)
        luty = luty/(projSamples/2/sensorFactor)

        # Set up the mapping
        lutxp, lutyp, J = self._aperture2image(inst, algo, zcCol, lutx, luty,
                                               projSamples, model)

        show_lutxyp = self._showProjection(lutxp, lutyp, sensorFactor,
                                           projSamples, raytrace=False)
        if (np.all(show_lutxyp <= 0)):
            self.caustic = True
            return

        # Calculate the weighting center (x, y) and radius
        realcx, realcy = self._image.getCenterAndR_ef()[0:2]

        # Extend the dimension of image by 20 pixel in x and y direction
        show_lutxyp = padArray(show_lutxyp, projSamples+20)

        # Get the binary matrix of image on pupil plane if raytrace=False
        struct0 = generate_binary_structure(2, 1)
        struct = iterate_structure(struct0, 4)
        struct = binary_dilation(struct, structure=struct0, iterations=2).astype(int)
        show_lutxyp = binary_dilation(show_lutxyp, structure=struct)
        show_lutxyp = binary_erosion(show_lutxyp, structure=struct)

        # Extract the region from the center of image and get the original one
        show_lutxyp = extractArray(show_lutxyp, projSamples)

        # Calculate the weighting center (x, y) and radius
        projcx, projcy = self._image.getCenterAndR_ef(image=show_lutxyp.astype(float))[0:2]

        # Shift the image to center of projection on pupil
        # +(-) means we need to move image to the right (left)
        shiftx = projcx - realcx
        # +(-) means we need to move image upward (downward)
        shifty = projcy - realcy

        self._image.updateImage(np.roll(self._image.getImg(), int(np.round(shifty)), axis=0))
        self._image.updateImage(np.roll(self._image.getImg(), int(np.round(shiftx)), axis=1))

        # Construct the interpolant to get the intensity on (x', p') plane
        # that corresponds to the grid points on (x,y)
        yp, xp = np.mgrid[-(sm/2 - 0.5):(sm/2 + 0.5), -(sm/2 - 0.5):(sm/2 + 0.5)]

        xp = xp/(sm/2/sensorFactor)
        yp = yp/(sm/2/sensorFactor)

        # Put the NaN to be 0 for the interpolate to use
        lutxp[np.isnan(lutxp)] = 0
        lutyp[np.isnan(lutyp)] = 0

        # Construct the function for interpolation
        ip = RectBivariateSpline(yp[:, 0], xp[0, :], self._image.getImg(), kx=1, ky=1)

        # Construct the projected image by the interpolation
        lutIp = np.zeros(lutxp.shape[0]*lutxp.shape[1])
        for ii, (xx, yy) in enumerate(zip(lutxp.ravel(), lutyp.ravel())):
            lutIp[ii] = ip(yy, xx)
        lutIp = lutIp.reshape(lutxp.shape)

        # Calaculate the image on focal plane with compensation based on flux
        # conservation
        # I(x, y)/I'(x', y') = J = (dx'/dx)*(dy'/dy) - (dx'/dy)*(dy'/dx)
        self._image.updateImage(lutIp * J)

        if (self.defocalType == DefocalType.Extra):
            self._image.updateImage(np.rot90(self._image.getImg(), k=2))

        # Put NaN to be 0
        holdedImg = self._image.getImg()
        holdedImg[np.isnan(holdedImg)] = 0
        self._image.updateImage(holdedImg)

        # Check the compensated image has the problem or not.
        # The negative value means the over-compensation from wavefront error
        if (np.any(self._image.getImg() < 0) and np.all(self.image0 >= 0)):
            print("WARNING: negative scale parameter, image is within caustic, zcCol (in um)=\n")
            self.caustic = True

        # Put the overcompensated part to be 0
        holdedImg = self._image.getImg()
        holdedImg[holdedImg < 0] = 0
        self._image.updateImage(holdedImg)
Beispiel #12
0
    def _padRandomImg(self, imgDim, padPixelSize):

        img = np.random.rand(imgDim, imgDim)
        imgPadded = padArray(img, imgDim + padPixelSize)

        return img, imgPadded
Beispiel #13
0
    def _createSignal(self, I1, I2, cliplevel):
        """Calculate the wavefront singal for "fft" to use in solving the
        Poisson's equation.

        Need to discuss the method to define threshold and discuss to use
        np.median() instead.
        Need to discuss why the calculation of I0 is different from "exp".

        Parameters
        ----------
        I1 : CompensableImage
            Intra- or extra-focal image.
        I2 : CompensableImage
            Intra- or extra-focal image.
        cliplevel : float
            Parameter to determine the threshold of calculating I0.

        Returns
        -------
        numpy.ndarray
            Approximated wavefront signal.
        """

        # Check the condition of images
        I1image, I2image = self._checkImageDim(I1, I2)

        # Wavefront signal S=-(1/I0)*(dI/dz) is approximated to be
        # -(1/delta z)*(I1-I2)/(I1+I2)
        num = I1image - I2image
        den = I1image + I2image

        # Define the effective minimum central signal element by the threshold
        # ( I0=(I1+I2)/2 )

        # Calculate the threshold
        pixelList = den * self.cMask
        pixelList = pixelList[pixelList != 0]

        low = pixelList.min()
        high = pixelList.max()
        medianThreshold = (high - low) / 2.0 + low

        # Define the effective minimum central signal element
        den[den < medianThreshold * cliplevel] = 1.5 * medianThreshold

        # Calculate delta z = f(f-l)/l, f: focal length, l: defocus distance of
        # the image planes
        focalLength = self._inst.getFocalLength()
        offset = self._inst.getDefocalDisOffset()
        deltaZ = focalLength * (focalLength - offset) / offset

        # Calculate the wavefront signal. Enforce the element outside the mask
        # to be 0.
        den[den == 0] = np.inf

        # Calculate the wavefront signal
        S = num / den / deltaZ

        # Extend the dimension of signal to the order of 2 for "fft" to use
        padDim = self.getFftDimension()
        Sout = padArray(S, padDim) * self.cMaskPad

        return Sout
Beispiel #14
0
    def _solvePoissonEq(self, I1, I2, iOutItr=0):
        """Solve the Poisson's equation by Fourier transform (differential) or
        serial expansion (integration).

        There is no convergence for fft actually. Need to add the difference
        comparison and X-alpha method. Need to discuss further for this.

        Parameters
        ----------
        I1 : CompensableImage
            Intra- or extra-focal image.
        I2 : CompensableImage
            Intra- or extra-focal image.
        iOutItr : int, optional
            ith number of outer loop iteration which is important in "fft"
            algorithm. (the default is 0.)

        Returns
        -------
        numpy.ndarray
            Coefficients of normal/ annular Zernike polynomials.
        numpy.ndarray
            Estimated wavefront.
        """

        # Calculate the aperture pixel size
        apertureDiameter = self._inst.getApertureDiameter()
        sensorFactor = self._inst.getSensorFactor()
        dimOfDonut = self._inst.getDimOfDonutOnSensor()
        aperturePixelSize = apertureDiameter * sensorFactor / dimOfDonut

        # Calculate the differential Omega
        dOmega = aperturePixelSize**2

        # Solve the Poisson's equation based on the type of algorithm
        numTerms = self.getNumOfZernikes()
        zobsR = self.getObsOfZernikes()
        PoissonSolver = self.getPoissonSolverName()
        if PoissonSolver == "fft":

            # Use the differential method by fft to solve the Poisson's
            # equation

            # Parameter to determine the threshold of calculating I0.
            sumclipSequence = self.getSignalClipSequence()
            cliplevel = sumclipSequence[iOutItr]

            # Generate the v, u-coordinates on pupil plane
            padDim = self.getFftDimension()
            v, u = np.mgrid[-0.5 / aperturePixelSize:0.5 /
                            aperturePixelSize:1.0 / padDim / aperturePixelSize,
                            -0.5 / aperturePixelSize:0.5 /
                            aperturePixelSize:1.0 / padDim /
                            aperturePixelSize, ]

            # Show the threshold and pupil coordinate information
            if self.debugLevel >= 3:
                print("iOuter=%d, cliplevel=%4.2f" % (iOutItr, cliplevel))
                print(v.shape)

            # Calculate the const of fft:
            # FT{Delta W} = -4*pi^2*(u^2+v^2) * FT{W}
            u2v2 = -4 * (np.pi**2) * (u * u + v * v)

            # Set origin to Inf to result in 0 at origin after filtering
            ctrIdx = int(np.floor(padDim / 2.0))
            u2v2[ctrIdx, ctrIdx] = np.inf

            # Calculate the wavefront signal
            Sini = self._createSignal(I1, I2, cliplevel)

            # Find the just-outside and just-inside indices of a ring in pixels
            # This is for the use in setting dWdn = 0
            boundaryT = self.getBoundaryThickness()

            struct = generate_binary_structure(2, 1)
            struct = iterate_structure(struct, boundaryT)

            ApringOut = np.logical_xor(
                binary_dilation(self.mask_pupil, structure=struct),
                self.mask_pupil).astype(int)
            ApringIn = np.logical_xor(
                binary_erosion(self.mask_pupil, structure=struct),
                self.mask_pupil).astype(int)

            bordery, borderx = np.nonzero(ApringOut)

            # Put the signal in boundary (since there's no existing Sestimate,
            # S just equals self.S as the initial condition of SCF
            S = Sini.copy()
            for jj in range(self.getNumOfInnerItr()):

                # Calculate FT{S}
                SFFT = np.fft.fftshift(np.fft.fft2(np.fft.fftshift(S)))

                # Calculate W by W=IFT{ FT{S}/(-4*pi^2*(u^2+v^2)) }
                W = np.fft.fftshift(
                    np.fft.irfft2(np.fft.fftshift(SFFT / u2v2), s=S.shape))

                # Estimate the wavefront (includes zeroing offset & masking to
                # the aperture size)

                # Take the estimated wavefront
                West = extractArray(W, dimOfDonut)

                # Calculate the offset
                offset = West[self.mask_pupil == 1].mean()
                West = West - offset
                West[self.mask_pupil == 0] = 0

                # Set dWestimate/dn = 0 around boundary
                WestdWdn0 = West.copy()

                # Do a 3x3 average around each border pixel, including only
                # those pixels inside the aperture. This averaging can be
                # efficiently computed using 1 numpy/scipy vectorized
                # convolve2d instruction to first sum the values in the 3x3
                # region, and dividing by a second convolve2d which counts
                # the non-zero pixels in each 3x3 region.

                kernel = np.ones((1 + 2 * boundaryT, 1 + 2 * boundaryT))
                tmp = convolve2d(West * ApringIn, kernel, mode="same")
                tmp /= convolve2d(ApringIn, kernel, mode="same")
                WestdWdn0[borderx, bordery] = tmp[borderx, bordery]

                # Take Laplacian to find sensor signal estimate (Delta W = S)
                del2W = laplace(WestdWdn0) / dOmega

                # Extend the dimension of signal to the order of 2 for "fft" to
                # use
                Sest = padArray(del2W, padDim)

                # Put signal back inside boundary, leaving the rest of
                # Sestimate
                Sest[self.mask_pupil_pad == 1] = Sini[self.mask_pupil_pad == 1]

                # Need to recheck this condition
                S = Sest

            # Calculate the coefficient of normal/ annular Zernike polynomials
            if self.getCompensatorMode() == "zer":
                xSensor, ySensor = self._inst.getSensorCoor()
                zc = ZernikeMaskedFit(West, xSensor, ySensor, numTerms,
                                      self.mask_pupil, zobsR)
            else:
                zc = np.zeros(numTerms)

        elif PoissonSolver == "exp":

            # Use the integration method by serial expansion to solve the
            # Poisson's equation

            # Calculate I0 and dI
            I0, dI = self._getdIandI(I1, I2)

            # Get the x, y coordinate in mask. The element outside mask is 0.
            xSensor, ySensor = self._inst.getSensorCoor()
            xSensor = xSensor * self.mask_comp
            ySensor = ySensor * self.mask_comp

            # Create the F matrix and Zernike-related matrixes

            # Get Zernike and gradient bases from cache.  These are each
            # (nzk, npix, npix) arrays, with the first dimension indicating
            # the Noll index.
            zk, dzkdx, dzkdy = self._zernikeBasisCache()

            # Eqn. (19) from Xin et al., Appl. Opt. 54, 9045-9054 (2015).
            # F_j = \int (d_z I) Z_j d_Omega
            F = np.tensordot(dI, zk, axes=((0, 1), (1, 2))) * dOmega
            # Eqn. (20) from Xin et al., Appl. Opt. 54, 9045-9054 (2015).
            # M_ij = \int I (grad Z_j) . (grad Z_i) d_Omega
            #      =   \int I (dZ_i/dx) (dZ_j/dx) d_Omega
            #        + \int I (dZ_i/dy) (dZ_j/dy) d_Omega
            Mij = np.einsum("ab,iab,jab->ij", I0, dzkdx, dzkdx)
            Mij += np.einsum("ab,iab,jab->ij", I0, dzkdy, dzkdy)
            Mij *= dOmega / (apertureDiameter / 2.0)**2

            # Calculate dz
            focalLength = self._inst.getFocalLength()
            offset = self._inst.getDefocalDisOffset()
            dz = 2 * focalLength * (focalLength - offset) / offset

            # Define zc
            zc = np.zeros(numTerms)

            # Consider specific Zk terms only
            idx = self.getZernikeTerms()

            # Solve the equation: M*W = F => W = M^(-1)*F
            zc_tmp = np.linalg.lstsq(Mij[:, idx][idx], F[idx],
                                     rcond=None)[0] / dz
            zc[idx] = zc_tmp

            # Estimate the wavefront surface based on z4 - z22
            # z0 - z3 are set to be 0 instead
            West = ZernikeAnnularEval(np.concatenate(([0, 0, 0], zc[3:])),
                                      xSensor, ySensor, zobsR)

        return zc, West
    def compensate(self, inst, algo, zcCol, model):
        """
        
        Calculate the image compensated from the affection of wavefront.
        
        Arguments:
            inst {[Instrument]} -- Instrument to use.
            algo {[Algorithm]} -- Algorithm to solve the Poisson's equation. It can by done 
                                  by the fast Fourier transform or serial expansion.
            zcCol {[float]} -- Coefficients of wavefront.
            model {[string]} -- Optical model. It can be "paraxial", "onAxis", or "offAxis".
        
        Raises:
            Exception -- Number of terms of normal/ annular Zernike polynomilas does 
                         not match the needed number for compensation to use.
        """

        # Check the condition of inputs
        numTerms = algo.parameter["numTerms"]
        if ((zcCol.ndim == 1) and (len(zcCol) != numTerms)):
            raise RuntimeError(
                "input:size",
                "zcCol in compensate needs to be a %d row column vector. \n" %
                numTerms)

        # Dimension of image
        sm, sn = self.__image.image.shape

        # Dimenstion of projected image on focal plane
        projSamples = sm

        # Let us create a look-up table for x -> xp first.
        luty, lutx = np.mgrid[-(projSamples / 2 - 0.5):(projSamples / 2 + 0.5),
                              -(projSamples / 2 - 0.5):(projSamples / 2 + 0.5)]

        sensorFactor = inst.parameter["sensorFactor"]
        lutx = lutx / (projSamples / 2 / sensorFactor)
        luty = luty / (projSamples / 2 / sensorFactor)

        # Set up the mapping
        lutxp, lutyp, J = self.__aperture2image(inst, algo, zcCol, lutx, luty,
                                                projSamples, model)

        show_lutxyp = self.__showProjection(lutxp,
                                            lutyp,
                                            sensorFactor,
                                            projSamples,
                                            raytrace=False)
        if (np.all(show_lutxyp <= 0)):
            self.caustic = True
            return

        # Calculate the weighting center (x, y) and radius
        realcx, realcy = self.__image.getCenterAndR_ef()[0:2]

        # Extend the dimension of image by 20 pixel in x and y direction
        show_lutxyp = padArray(show_lutxyp, projSamples + 20)

        # Get the binary matrix of image on pupil plane if raytrace=False
        struct0 = generate_binary_structure(2, 1)
        struct = iterate_structure(struct0, 4)
        struct = binary_dilation(struct, structure=struct0,
                                 iterations=2).astype(int)
        show_lutxyp = binary_dilation(show_lutxyp, structure=struct)
        show_lutxyp = binary_erosion(show_lutxyp, structure=struct)

        # Extract the region from the center of image and get the original one
        show_lutxyp = extractArray(show_lutxyp, projSamples)

        # Calculate the weighting center (x, y) and radius
        projcx, projcy = self.__image.getCenterAndR_ef(
            image=show_lutxyp.astype(float))[0:2]

        # Shift the image to center of projection on pupil
        # +(-) means we need to move image to the right (left)
        shiftx = projcx - realcx
        # +(-) means we need to move image upward (downward)
        shifty = projcy - realcy

        self.__image.image = np.roll(self.__image.image,
                                     int(np.round(shifty)),
                                     axis=0)
        self.__image.image = np.roll(self.__image.image,
                                     int(np.round(shiftx)),
                                     axis=1)

        # Construct the interpolant to get the intensity on (x', p') plane
        # that corresponds to the grid points on (x,y)
        yp, xp = np.mgrid[-(sm / 2 - 0.5):(sm / 2 + 0.5),
                          -(sm / 2 - 0.5):(sm / 2 + 0.5)]

        xp = xp / (sm / 2 / sensorFactor)
        yp = yp / (sm / 2 / sensorFactor)

        # Put the NaN to be 0 for the interpolate to use
        lutxp[np.isnan(lutxp)] = 0
        lutyp[np.isnan(lutyp)] = 0

        # Construct the function for interpolation
        ip = RectBivariateSpline(yp[:, 0],
                                 xp[0, :],
                                 self.__image.image,
                                 kx=1,
                                 ky=1)

        # Construct the projected image by the interpolation
        lutIp = np.zeros(lutxp.shape[0] * lutxp.shape[1])
        for ii, (xx, yy) in enumerate(zip(lutxp.ravel(), lutyp.ravel())):
            lutIp[ii] = ip(yy, xx)
        lutIp = lutIp.reshape(lutxp.shape)

        # Calaculate the image on focal plane with compensation based on flux conservation
        # I(x, y)/I'(x', y') = J = (dx'/dx)*(dy'/dy) - (dx'/dy)*(dy'/dx)
        self.__image.image = lutIp * J

        if (self.atype == "extra"):
            self.__image.image = np.rot90(self.__image.image, k=2)

        # Put NaN to be 0
        self.__image.image[np.isnan(self.__image.image)] = 0

        # Check the compensated image has the problem or not.
        # The negative value means the over-compensation from wavefront error
        if (np.any(self.__image.image < 0) and np.all(self.image0 >= 0)):
            print(
                "WARNING: negative scale parameter, image is within caustic, zcCol (in um)=\n"
            )
            self.caustic = True

        # Put the overcompensated part to be 0.
        self.__image.image[self.__image.image < 0] = 0
Beispiel #16
0
    def _solvePoissonEq(self, I1, I2, iOutItr=0):
        """Solve the Poisson's equation by Fourier transform (differential) or
        serial expansion (integration).

        There is no convergence for fft actually. Need to add the difference
        comparison and X-alpha method. Need to discuss further for this.

        Parameters
        ----------
        I1 : Image
            Intra- or extra-focal image.
        I2 : Image
            Intra- or extra-focal image.
        iOutItr : int, optional
            ith number of outer loop iteration which is important in "fft"
            algorithm. (the default is 0.)

        Returns
        -------
        numpy.ndarray
            Coefficients of normal/ annular Zernike polynomials.
        numpy.ndarray
            Estimated wavefront.
        """

        # Calculate the aperature pixel size
        apertureDiameter = self._inst.getApertureDiameter()
        sensorFactor = self._inst.getSensorFactor()
        dimOfDonut = self._inst.getDimOfDonutOnSensor()
        aperturePixelSize = apertureDiameter*sensorFactor/dimOfDonut

        # Calculate the differential Omega
        dOmega = aperturePixelSize**2

        # Solve the Poisson's equation based on the type of algorithm
        numTerms = self.getNumOfZernikes()
        zobsR = self.getObsOfZernikes()
        PoissonSolver = self.getPoissonSolverName()
        if (PoissonSolver == "fft"):

            # Use the differential method by fft to solve the Poisson's
            # equation

            # Parameter to determine the threshold of calculating I0.
            sumclipSequence = self.getSignalClipSequence()
            cliplevel = sumclipSequence[iOutItr]

            # Generate the v, u-coordinates on pupil plane
            padDim = self.getFftDimension()
            v, u = np.mgrid[
                -0.5/aperturePixelSize: 0.5/aperturePixelSize: 1./padDim/aperturePixelSize,
                -0.5/aperturePixelSize: 0.5/aperturePixelSize: 1./padDim/aperturePixelSize]

            # Show the threshold and pupil coordinate information
            if (self.debugLevel >= 3):
                print("iOuter=%d, cliplevel=%4.2f" % (iOutItr, cliplevel))
                print(v.shape)

            # Calculate the const of fft:
            # FT{Delta W} = -4*pi^2*(u^2+v^2) * FT{W}
            u2v2 = -4 * (np.pi**2) * (u*u + v*v)

            # Set origin to Inf to result in 0 at origin after filtering
            ctrIdx = int(np.floor(padDim/2.0))
            u2v2[ctrIdx, ctrIdx] = np.inf

            # Calculate the wavefront signal
            Sini = self._createSignal(I1, I2, cliplevel)

            # Find the just-outside and just-inside indices of a ring in pixels
            # This is for the use in setting dWdn = 0
            boundaryT = self.getBoundaryThickness()

            struct = generate_binary_structure(2, 1)
            struct = iterate_structure(struct, boundaryT)

            ApringOut = np.logical_xor(binary_dilation(self.pMask, structure=struct),
                                       self.pMask).astype(int)
            ApringIn = np.logical_xor(binary_erosion(self.pMask, structure=struct),
                                      self.pMask).astype(int)

            bordery, borderx = np.nonzero(ApringOut)

            # Put the signal in boundary (since there's no existing Sestimate,
            # S just equals self.S as the initial condition of SCF
            S = Sini.copy()
            for jj in range(self.getNumOfInnerItr()):

                # Calculate FT{S}
                SFFT = np.fft.fftshift(np.fft.fft2(np.fft.fftshift(S)))

                # Calculate W by W=IFT{ FT{S}/(-4*pi^2*(u^2+v^2)) }
                W = np.fft.fftshift(np.fft.irfft2(np.fft.fftshift(SFFT/u2v2), s=S.shape))

                # Estimate the wavefront (includes zeroing offset & masking to
                # the aperture size)

                # Take the estimated wavefront
                West = extractArray(W, dimOfDonut)

                # Calculate the offset
                offset = West[self.pMask == 1].mean()
                West = West - offset
                West[self.pMask == 0] = 0

                # Set dWestimate/dn = 0 around boundary
                WestdWdn0 = West.copy()

                # Do a 3x3 average around each border pixel, including only
                # those pixels inside the aperture
                for ii in range(len(borderx)):
                    reg = West[borderx[ii] - boundaryT:
                               borderx[ii] + boundaryT + 1,
                               bordery[ii] - boundaryT:
                               bordery[ii] + boundaryT + 1]

                    intersectIdx = ApringIn[borderx[ii] - boundaryT:
                                            borderx[ii] + boundaryT + 1,
                                            bordery[ii] - boundaryT:
                                            bordery[ii] + boundaryT + 1]

                    WestdWdn0[borderx[ii], bordery[ii]] = \
                        reg[np.nonzero(intersectIdx)].mean()

                # Take Laplacian to find sensor signal estimate (Delta W = S)
                del2W = laplace(WestdWdn0)/dOmega

                # Extend the dimension of signal to the order of 2 for "fft" to
                # use
                Sest = padArray(del2W, padDim)

                # Put signal back inside boundary, leaving the rest of
                # Sestimate
                Sest[self.pMaskPad == 1] = Sini[self.pMaskPad == 1]

                # Need to recheck this condition
                S = Sest

            # Define the estimated wavefront
            # self.West = West.copy()

            # Calculate the coefficient of normal/ annular Zernike polynomials
            if (self.getCompensatorMode() == "zer"):
                xSensor, ySensor = self._inst.getSensorCoor()
                zc = ZernikeMaskedFit(West, xSensor, ySensor, numTerms,
                                      self.pMask, zobsR)
            else:
                zc = np.zeros(numTerms)

        elif (PoissonSolver == "exp"):

            # Use the integration method by serial expansion to solve the
            # Poisson's equation

            # Calculate I0 and dI
            I0, dI = self._getdIandI(I1, I2)

            # Get the x, y coordinate in mask. The element outside mask is 0.
            xSensor, ySensor = self._inst.getSensorCoor()
            xSensor = xSensor * self.cMask
            ySensor = ySensor * self.cMask

            # Create the F matrix and Zernike-related matrixes
            F = np.zeros(numTerms)
            dZidx = np.zeros((numTerms, dimOfDonut, dimOfDonut))
            dZidy = dZidx.copy()

            zcCol = np.zeros(numTerms)
            for ii in range(int(numTerms)):

                # Calculate the matrix for each Zk related component
                # Set the specific Zk cofficient to be 1 for the calculation
                zcCol[ii] = 1

                F[ii] = np.sum(dI*ZernikeAnnularEval(zcCol, xSensor, ySensor, zobsR))*dOmega
                dZidx[ii, :, :] = ZernikeAnnularGrad(zcCol, xSensor, ySensor, zobsR, "dx")
                dZidy[ii, :, :] = ZernikeAnnularGrad(zcCol, xSensor, ySensor, zobsR, "dy")

                # Set the specific Zk cofficient back to 0 to avoid interfering
                # other Zk's calculation
                zcCol[ii] = 0

            # Calculate Mij matrix, need to check the stability of integration
            # and symmetry later
            Mij = np.zeros([numTerms, numTerms])
            for ii in range(numTerms):
                for jj in range(numTerms):
                    Mij[ii, jj] = np.sum(I0*(dZidx[ii, :, :].squeeze()*dZidx[jj, :, :].squeeze() +
                                             dZidy[ii, :, :].squeeze()*dZidy[jj, :, :].squeeze()))
            Mij = dOmega/(apertureDiameter/2.)**2 * Mij

            # Calculate dz
            focalLength = self._inst.getFocalLength()
            offset = self._inst.getDefocalDisOffset()
            dz = 2*focalLength*(focalLength-offset)/offset

            # Define zc
            zc = np.zeros(numTerms)

            # Consider specific Zk terms only
            idx = (self.getZernikeTerms() - 1).tolist()

            # Solve the equation: M*W = F => W = M^(-1)*F
            zc_tmp = np.linalg.lstsq(Mij[:, idx][idx], F[idx], rcond=None)[0]/dz
            zc[idx] = zc_tmp

            # Estimate the wavefront surface based on z4 - z22
            # z0 - z3 are set to be 0 instead
            West = ZernikeAnnularEval(np.concatenate(([0, 0, 0], zc[3:])),
                                      xSensor, ySensor, zobsR)

        return zc, West
Beispiel #17
0
    def __solvePoissonEq(self, inst, I1, I2, iOutItr=0):
        """

        Solve the Poisson's equation by Fourier transform (differential) or serial expansion
        (integration).

        There is no convergence for fft actually. Need to add the difference comparison and
        Xa method. Need to discuss further for this.

        Arguments:
            inst {[Instrument]} -- Instrument to use.
            I1 {[Image]} -- Intra- or extra-focal image.
            I2 {[Image]} -- Intra- or extra-focal image.

        Keyword Arguments:
            iOutItr {[int]} -- ith number of outer loop iteration which is important
                               in "fft" algorithm (default: {0}).

        Returns:
            [float] -- Coefficients of normal/ annular Zernike polynomials.
            [float] -- Estimated wavefront.
        """

        # Calculate the aperature pixel size
        apertureDiameter = inst.parameter["apertureDiameter"]
        sensorFactor = inst.parameter["sensorFactor"]
        sensorSamples = inst.parameter["sensorSamples"]
        aperturePixelSize = apertureDiameter * sensorFactor / sensorSamples

        # Calculate the differential Omega
        dOmega = aperturePixelSize**2

        # Solve the Poisson's equation based on the type of algorithm
        numTerms = self.parameter["numTerms"]
        zobsR = self.parameter["zobsR"]
        PoissonSolver = self.parameter["PoissonSolver"]
        if (PoissonSolver == "fft"):

            # Use the differential method by fft to solve the Poisson's equation

            # Parameter to determine the threshold of calculating I0.
            sumclipSequence = self.parameter["sumclipSequence"]
            cliplevel = sumclipSequence[iOutItr]

            # Generate the v, u-coordinates on pupil plane
            padDim = self.parameter["padDim"]
            v, u = np.mgrid[-0.5 / aperturePixelSize:0.5 /
                            aperturePixelSize:1. / padDim / aperturePixelSize,
                            -0.5 / aperturePixelSize:0.5 /
                            aperturePixelSize:1. / padDim / aperturePixelSize]

            # Show the threshold and pupil coordinate information
            if (self.debugLevel >= 3):
                print("iOuter=%d, cliplevel=%4.2f" % (iOutItr, cliplevel))
                print(v.shape)

            # Calculate the const of fft: FT{Delta W} = -4*pi^2*(u^2+v^2) * FT{W}
            u2v2 = -4 * (np.pi**2) * (u * u + v * v)

            # Set origin to Inf to result in 0 at origin after filtering
            ctrIdx = int(np.floor(padDim / 2.0))
            u2v2[ctrIdx, ctrIdx] = np.inf

            # Calculate the wavefront signal
            Sini = self.__createSignal(inst, I1, I2, cliplevel)

            # Find the just-outside and just-inside indices of a ring in pixels
            # This is for the use in setting dWdn = 0
            boundaryT = self.parameter["boundaryT"]

            struct = generate_binary_structure(2, 1)
            struct = iterate_structure(struct, boundaryT)

            ApringOut = np.logical_xor(
                binary_dilation(self.pMask, structure=struct),
                self.pMask).astype(int)
            ApringIn = np.logical_xor(
                binary_erosion(self.pMask, structure=struct),
                self.pMask).astype(int)

            bordery, borderx = np.nonzero(ApringOut)

            # Put the signal in boundary (since there's no existing Sestimate, S just equals self.S
            # as the initial condition of SCF
            S = Sini.copy()
            for jj in range(int(self.parameter["innerItr"])):

                # Calculate FT{S}
                SFFT = np.fft.fftshift(np.fft.fft2(np.fft.fftshift(S)))

                # Calculate W by W=IFT{ FT{S}/(-4*pi^2*(u^2+v^2)) }
                W = np.fft.fftshift(
                    np.fft.irfft2(np.fft.fftshift(SFFT / u2v2), s=S.shape))

                # Estimate the wavefront (includes zeroing offset & masking to the aperture size)

                # Take the estimated wavefront
                West = extractArray(W, sensorSamples)

                # Calculate the offset
                offset = West[self.pMask == 1].mean()
                West = West - offset
                West[self.pMask == 0] = 0

                # Set dWestimate/dn = 0 around boundary
                WestdWdn0 = West.copy()

                # Do a 3x3 average around each border pixel, including only those pixels
                # inside the aperture
                for ii in range(len(borderx)):
                    reg = West[borderx[ii] - boundaryT:borderx[ii] +
                               boundaryT + 1, bordery[ii] -
                               boundaryT:bordery[ii] + boundaryT + 1]

                    intersectIdx = ApringIn[borderx[ii] -
                                            boundaryT:borderx[ii] + boundaryT +
                                            1, bordery[ii] -
                                            boundaryT:bordery[ii] + boundaryT +
                                            1]

                    WestdWdn0[borderx[ii], bordery[ii]] = reg[np.nonzero(
                        intersectIdx)].mean()

                # Take Laplacian to find sensor signal estimate (Delta W = S)
                del2W = laplace(WestdWdn0) / dOmega

                # Extend the dimension of signal to the order of 2 for "fft" to use
                Sest = padArray(del2W, padDim)

                # Put signal back inside boundary, leaving the rest of Sestimate
                Sest[self.pMaskPad == 1] = Sini[self.pMaskPad == 1]

                # Need to recheck this condition
                S = Sest

            # Define the estimated wavefront
            # self.West = West.copy()

            # Calculate the coefficient of normal/ annular Zernike polynomials
            if (self.parameter["compMode"] == "zer"):
                zc = ZernikeMaskedFit(West, inst.xSensor, inst.ySensor,
                                      numTerms, self.pMask, zobsR)
            else:
                zc = np.zeros(numTerms)

        elif (PoissonSolver == "exp"):

            # Use the integration method by serial expansion to solve the Poisson's equation

            # Calculate I0 and dI
            I0, dI = self.__getdIandI(I1, I2)

            # Get the x, y coordinate in mask. The element outside mask is 0.
            xSensor = inst.xSensor * self.cMask
            ySensor = inst.ySensor * self.cMask

            # Create the F matrix and Zernike-related matrixes
            F = np.zeros(numTerms)
            dZidx = np.zeros([numTerms, sensorSamples, sensorSamples])
            dZidy = dZidx.copy()

            zcCol = np.zeros(numTerms)
            for ii in range(int(numTerms)):

                # Calculate the matrix for each Zk related component
                # Set the specific Zk cofficient to be 1 for the calculation
                zcCol[ii] = 1

                F[ii] = np.sum(dI * ZernikeAnnularEval(zcCol, xSensor, ySensor,
                                                       zobsR)) * dOmega
                dZidx[ii, :, :] = ZernikeAnnularGrad(zcCol, xSensor, ySensor,
                                                     zobsR, "dx")
                dZidy[ii, :, :] = ZernikeAnnularGrad(zcCol, xSensor, ySensor,
                                                     zobsR, "dy")

                # Set the specific Zk cofficient back to 0 to avoid interfering other Zk's calculation
                zcCol[ii] = 0

            # Calculate Mij matrix, need to check the stability of integration and symmetry later
            Mij = np.zeros([numTerms, numTerms])
            for ii in range(numTerms):
                for jj in range(numTerms):
                    Mij[ii, jj] = np.sum(I0 * (
                        dZidx[ii, :, :].squeeze() * dZidx[jj, :, :].squeeze() +
                        dZidy[ii, :, :].squeeze() * dZidy[jj, :, :].squeeze()))
            Mij = dOmega / (apertureDiameter / 2.)**2 * Mij

            # Calculate dz
            focalLength = inst.parameter["focalLength"]
            offset = inst.parameter["offset"]
            dz = 2 * focalLength * (focalLength - offset) / offset

            # Define zc
            zc = np.zeros(numTerms)

            # Consider specific Zk terms only
            idx = [x - 1 for x in self.parameter["ZTerms"]]

            # Solve the equation: M*W = F => W = M^(-1)*F
            zc_tmp = np.linalg.lstsq(Mij[:, idx][idx], F[idx],
                                     rcond=None)[0] / dz
            zc[idx] = zc_tmp

            # Estimate the wavefront surface based on z4 - z22
            # z0 - z3 are set to be 0 instead
            West = ZernikeAnnularEval(np.concatenate(([0, 0, 0], zc[3:])),
                                      xSensor, ySensor, zobsR)

        return zc, West
Beispiel #18
0
    def compensate(self, inst, algo, zcCol, model):
        """Calculate the image compensated from the affection of wavefront.

        Parameters
        ----------
        inst : Instrument
            Instrument to use.
        algo : Algorithm
            Algorithm to solve the Poisson's equation. It can by done by the
            fast Fourier transform or serial expansion.
        zcCol : numpy.ndarray
            Coefficients of wavefront.
        model : str
            Optical model. It can be "paraxial", "onAxis", or "offAxis".

        Raises
        ------
        RuntimeError
            input:size zcCol in compensate needs to be a numTerms row column
            vector.
        """

        # Check the condition of inputs
        numTerms = algo.getNumOfZernikes()
        if (zcCol.ndim == 1) and (len(zcCol) != numTerms):
            raise RuntimeError(
                "input:size",
                "zcCol in compensate needs to be a %d row column vector. \n" %
                numTerms,
            )

        # Dimension of image
        sm, sn = self.getImg().shape

        # Dimension of projected image on focal plane
        projSamples = sm

        # Let us create a look-up table for x -> xp first.
        luty, lutx = np.mgrid[-(projSamples / 2 - 0.5):(projSamples / 2 + 0.5),
                              -(projSamples / 2 - 0.5):(projSamples / 2 +
                                                        0.5), ]

        sensorFactor = inst.getSensorFactor()
        lutx = lutx / (projSamples / 2 / sensorFactor)
        luty = luty / (projSamples / 2 / sensorFactor)

        # Set up the mapping
        lutxp, lutyp, J = self._aperture2image(inst, algo, zcCol, lutx, luty,
                                               projSamples, model)

        show_lutxyp = self._showProjection(lutxp,
                                           lutyp,
                                           sensorFactor,
                                           projSamples,
                                           raytrace=False)
        if np.all(show_lutxyp <= 0):
            self.caustic = True
            return

        # Extend the dimension of image by 20 pixel in x and y direction
        show_lutxyp = padArray(show_lutxyp, projSamples + 20)

        # Get the binary matrix of image on pupil plane if raytrace=False
        struct0 = generate_binary_structure(2, 1)
        struct = iterate_structure(struct0, 4)
        struct = binary_dilation(struct, structure=struct0,
                                 iterations=2).astype(int)
        show_lutxyp = binary_dilation(show_lutxyp, structure=struct)
        show_lutxyp = binary_erosion(show_lutxyp, structure=struct)

        # Extract the region from the center of image and get the original one
        show_lutxyp = extractArray(show_lutxyp, projSamples)

        # Recenter the image
        imgRecenter = self.centerOnProjection(self.getImg(),
                                              show_lutxyp.astype(float),
                                              window=20)
        self.updateImage(imgRecenter)

        # Construct the interpolant to get the intensity on (x', p') plane
        # that corresponds to the grid points on (x,y)
        yp, xp = np.mgrid[-(sm / 2 - 0.5):(sm / 2 + 0.5),
                          -(sm / 2 - 0.5):(sm / 2 + 0.5)]

        xp = xp / (sm / 2 / sensorFactor)
        yp = yp / (sm / 2 / sensorFactor)

        # Put the NaN to be 0 for the interpolate to use
        lutxp[np.isnan(lutxp)] = 0
        lutyp[np.isnan(lutyp)] = 0

        # Construct the function for interpolation
        ip = RectBivariateSpline(yp[:, 0], xp[0, :], self.getImg(), kx=1, ky=1)

        # Construct the projected image by the interpolation
        lutIp = ip(lutyp, lutxp, grid=False)

        # Calculate the image on focal plane with compensation based on flux
        # conservation
        # I(x, y)/I'(x', y') = J = (dx'/dx)*(dy'/dy) - (dx'/dy)*(dy'/dx)
        self.updateImage(lutIp * J)

        if self.defocalType == DefocalType.Extra:
            self.updateImage(np.rot90(self.getImg(), k=2))

        # Put NaN to be 0
        imgCompensate = self.getImg()
        imgCompensate[np.isnan(imgCompensate)] = 0

        # Check the compensated image has the problem or not.
        # The negative value means the over-compensation from wavefront error
        if np.any(imgCompensate < 0) and np.all(self.image0 >= 0):
            print(
                "WARNING: negative scale parameter, image is within caustic, zcCol (in um)=\n"
            )
            self.caustic = True

        # Put the overcompensated part to be 0
        imgCompensate[imgCompensate < 0] = 0
        self.updateImage(imgCompensate)
Beispiel #19
0
    def _padRandomImg(self, imgDim, padPixelSize):

        img = np.random.rand(imgDim, imgDim)
        imgPadded = padArray(img, imgDim + padPixelSize)

        return img, imgPadded