Beispiel #1
0
    def testExtractArray(self):

        imgDim = 10
        padPixelSize = 20
        img, imgPadded = self._padRandomImg(imgDim, padPixelSize)

        imgExtracted = extractArray(imgPadded, imgDim)

        self.assertEqual(imgExtracted.shape[0], imgDim)
Beispiel #2
0
    def testExtractArray(self):

        imgDim = 10
        padPixelSize = 20
        img, imgPadded = self._padRandomImg(imgDim, padPixelSize)

        imgExtracted = extractArray(imgPadded, imgDim)

        self.assertEqual(imgExtracted.shape[0], imgDim)
Beispiel #3
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 #4
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 #5
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 #6
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
    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 #8
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 #9
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
Beispiel #10
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)