Example #1
0
    def _genOrthogonalFunc(self, r, theta, z1, z2, e):

        func = (
            r *
            ZernikeAnnularEval(z1, r * np.cos(theta), r * np.sin(theta), e) *
            ZernikeAnnularEval(z2, r * np.cos(theta), r * np.sin(theta), e))

        return func
def createZernikeList(nz, x, y, e):
    '''Create a list of masked zernike images'''
    zimages = []
    for i in range(nz):
        z = np.zeros(nz)
        z[i] = 1.
        zimages.append(ZernikeAnnularEval(z, x, y, e))
    return zimages
Example #3
0
    def testZernikeMaskFit(self):
        e = 0.2
        nc = 6
        surface = ZernikeAnnularEval(self.zerCoef[0:nc], self.xx, self.yy, e)

        # mask data
        cut = -0.9
        r = np.sqrt(self.xx**2 + self.yy**2)
        idx = (r > 1) | (r < e) | (self.xx < cut)

        xx = self.xx[:].copy()
        yy = self.yy[:].copy()
        xx[idx] = np.nan
        yy[idx] = np.nan
        mask = ~np.isnan(xx)

        zr = ZernikeMaskedFit(surface, xx, yy, nc, mask, e)

        self.assertLess(np.sum(np.abs(zr - self.zerCoef[0:nc]) ** 2), 1e-10)
Example #4
0
    def getPrintthz(self, zAngleInRadian, preCompElevInRadian=0):
        """Get the mirror print in m along z direction in specific zenith
        angle.

        FEA: Finite element analysis.

        Parameters
        ----------
        zAngleInRadian : float
            Zenith angle in radian.
        preCompElevInRadian : float, optional
            Pre-compensation elevation angle in radian. (the default is 0.)

        Returns
        -------
        numpy.ndarray
            Corrected projection in m along z direction.
        """

        # Data needed to determine gravitational print through
        data = self._feaZenFile.getMatContent()
        zdx = data[:, 0]
        zdy = data[:, 1]
        zdz = data[:, 2]

        data = self._feaHorFile.getMatContent()
        hdx = data[:, 0]
        hdy = data[:, 1]
        hdz = data[:, 2]

        # Do the M1M3 gravitational correction.
        # Map the changes of dx, dy, and dz on a plane for certain zenith angle
        printthxInM = zdx * np.cos(zAngleInRadian) + \
            hdx * np.sin(zAngleInRadian)
        printthyInM = zdy * np.cos(zAngleInRadian) + \
            hdy * np.sin(zAngleInRadian)
        printthzInM = zdz * np.cos(zAngleInRadian) + \
            hdz * np.sin(zAngleInRadian)

        # Get the bending mode information
        idx1, idx3, bx, by, bz = self._getMirCoor()

        # Calcualte the mirror ideal shape
        zRef = self._calcIdealShape(bx * 1000, by * 1000, idx1, idx3) / 1000

        # Calcualte the mirror ideal shape with the displacement
        zpRef = self._calcIdealShape(
            (bx + printthxInM) * 1000,
            (by + printthyInM) * 1000, idx1, idx3) / 1000

        # Convert printthz into surface sag to get the estimated wavefront
        # error.
        # Do the zenith angle correction by the linear approximation with the
        # ideal shape.
        printthzInM = printthzInM - (zpRef - zRef)

        # Normalize the coordinate
        Ri = self.getInnerRinM()[0]
        R = self.getOuterRinM()[0]

        normX = bx / R
        normY = by / R
        obs = Ri / R

        # Fit the annular Zernike polynomials z0-z2 (piton, x-tilt, y-tilt)
        zc = ZernikeAnnularFit(printthzInM, normX, normY, 3, obs)

        # Do the estimated wavefront error correction for the mirror projection
        printthzInM -= ZernikeAnnularEval(zc, normX, normY, obs)

        return printthzInM
Example #5
0
    def getPrintthz(self,
                    zAngleInRadian,
                    preCompElevInRadian=0,
                    FEAzenFileName="M1M3_dxdydz_zenith.txt",
                    FEAhorFileName="M1M3_dxdydz_horizon.txt",
                    gridFileName="M1M3_1um_156_grid.DAT"):
        """
        
        Get the mirror print in m along z direction in specific zenith angle.
        
        Arguments:
            zAngleInRadian {[float]} -- Zenith angle in radian.
        
        Keyword Arguments:
            preCompElevInRadian {float} -- Pre-compensation elevation angle in radian. 
                                           (default: {0})
            FEAzenFileName {str} -- Finite element analysis (FEA) model data file name in zenith angle. 
                                    (default: {"M1M3_dxdydz_zenith.txt"})                
            FEAhorFileName {str} -- Finite element analysis (FEA) model data file name in horizon angle. 
                                    (default: {"M1M3_dxdydz_horizon.txt"})
            gridFileName {str} -- File name of bending mode data. (default: {"M1M3_1um_156_grid.DAT"})
        
        Returns:
            [ndarray] -- Corrected projection in m along z direction.
        """

        # Data needed to determine gravitational print through
        data = self.getMirrorData(FEAzenFileName)
        zdx = data[:, 0]
        zdy = data[:, 1]
        zdz = data[:, 2]

        data = self.getMirrorData(FEAhorFileName)
        hdx = data[:, 0]
        hdy = data[:, 1]
        hdz = data[:, 2]

        # Do the M1M3 gravitational correction.
        # Map the changes of dx, dy, and dz on a plane for certain zenith angle
        printthxInM = zdx * np.cos(zAngleInRadian) + hdx * np.sin(
            zAngleInRadian)
        printthyInM = zdy * np.cos(zAngleInRadian) + hdy * np.sin(
            zAngleInRadian)
        printthzInM = zdz * np.cos(zAngleInRadian) + hdz * np.sin(
            zAngleInRadian)

        # Get the bending mode information
        idx1, idx3, bx, by, bz = self.__getMirCoor(gridFileName=gridFileName)

        # Calcualte the mirror ideal shape
        zRef = self.__idealShape(bx * 1000, by * 1000, idx1, idx3) / 1000

        # Calcualte the mirror ideal shape with the displacement
        zpRef = self.__idealShape((bx + printthxInM) * 1000,
                                  (by + printthyInM) * 1000, idx1, idx3) / 1000

        # Convert printthz into surface sag to get the estimated wavefront error
        # Do the zenith angle correction by the linear approximation with the ideal shape
        printthzInM = printthzInM - (zpRef - zRef)

        # Normalize the coordinate
        R = self.RinM[0]
        Ri = self.RiInM[0]
        normX = bx / R
        normY = by / R
        obs = Ri / R

        # Fit the annular Zernike polynomials z0-z2 (piton, x-tilt, y-tilt)
        zc = ZernikeAnnularFit(printthzInM, normX, normY, 3, obs)

        # Do the estimated wavefront error correction for the mirror projection
        printthzInM -= ZernikeAnnularEval(zc, normX, normY, obs)

        return printthzInM
Example #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
Example #7
0
    def __singleItr(self, inst, I1, I2, model, tol=1e-3):
        """

        Run the outer-loop with single iteration to solve the transport of intensity equation (TIE).
        This is to compensate the approximation of wavefront S = -1/(delta Z) * (I1 - I2)/ (I1 + I2)).

        Arguments:
            inst {[Instrument]} -- Instrument to use.
            I1 {[Image]} -- Intra- or extra-focal image.
            I2 {[Image]} -- Intra- or extra-focal image.
            model {[string]} -- Optical model. It can be "paraxial", "onAxis", or "offAxis".
            tol {[float]} -- Tolerance of difference of coefficients of Zk polynomials compared with
                             the previours iteration.
        """

        # Use the zonal mode ("zer")
        compMode = self.parameter["compMode"]

        # Define the gain of feedbackGain
        feedbackGain = self.parameter["feedbackGain"]

        # Set the pre-condition
        if (self.currentItr == 0):

            # Check this is the first time of running iteration or not
            if (I1.image0 is None or I2.image0 is None):

                # Check the image dimension
                if (I1.image.shape != I2.image.shape):
                    print(
                        "Error: The intra and extra image stamps need to be of same size."
                    )
                    sys.exit()

                # Calculate the pupil mask (binary matrix) and related parameters
                I1.makeMask(inst, model, self.parameter["boundaryT"], 1)
                I2.makeMask(inst, model, self.parameter["boundaryT"], 1)
                self.__makeMasterMask(I1, I2, self.parameter["PoissonSolver"])

                # Load the offAxis correction coefficients
                if (model == "offAxis"):
                    instDir = os.path.join(inst.instDir, inst.instName)
                    I1.getOffAxisCorr(instDir,
                                      self.parameter["offAxisPolyOrder"])
                    I2.getOffAxisCorr(instDir,
                                      self.parameter["offAxisPolyOrder"])

                # Cocenter the images to the center referenced to fieldX and fieldY. Need to check the
                # availability of this.
                I1.imageCoCenter(inst, debugLevel=self.debugLevel)
                I2.imageCoCenter(inst, debugLevel=self.debugLevel)

                # Update the self-initial image
                I1.updateImage0()
                I2.updateImage0()

            # Initialize the variables used in the iteration.
            self.zcomp = np.zeros(self.parameter["numTerms"])
            self.zc = self.zcomp.copy()

            sensorSamples = inst.parameter["sensorSamples"]
            self.wcomp = np.zeros([sensorSamples, sensorSamples])
            self.West = self.wcomp.copy()

            self.caustic = False

        # Rename this index (currentItr) for the simplification
        jj = self.currentItr

        # Solve the transport of intensity equation (TIE)
        if (not self.caustic):

            # Reset the images before the compensation
            I1.updateImage(I1.image0.copy())
            I2.updateImage(I2.image0.copy())

            if (compMode == "zer"):

                # Zk coefficient from the previous iteration
                ztmp = self.zc

                # Do the feedback of Zk from the lower terms first based on the
                # sequence defined in compSequence
                if (jj != 0):
                    compSequence = self.parameter["compSequence"]
                    ztmp[int(compSequence[jj - 1]):] = 0

                # Add partial feedback of residual estimated wavefront in Zk
                self.zcomp = self.zcomp + ztmp * feedbackGain

                # Remove the image distortion if the optical model is not "paraxial"
                # Only the optical model of "onAxis" or "offAxis" is considered here
                I1.compensate(inst, self, self.zcomp, model)
                I2.compensate(inst, self, self.zcomp, model)

            # Check the image condition. If there is the problem, done with this __singleItr().
            if (I1.caustic == True or I2.caustic == True):
                self.converge[:, jj] = self.converge[:, jj - 1]
                self.caustic = True
                return

            # Correct the defocal images if I1 and I2 are belong to different
            # sources, which is determined by the (fieldX, field Y)
            I1, I2 = self.__applyI1I2pMask(I1, I2)

            # Solve the Poisson's equation
            self.zc, self.West = self.__solvePoissonEq(inst, I1, I2, jj)

            # Record/ calculate the Zk coefficient and wavefront
            if (compMode == "zer"):
                self.converge[:, jj] = self.zcomp + self.zc
                self.wcomp = self.West + ZernikeAnnularEval(
                    np.concatenate(([0, 0, 0], self.zcomp[3:])), inst.xoSensor,
                    inst.yoSensor, self.parameter["zobsR"])

        else:
            # Once we run into caustic, stop here, results may be close to real aberration.
            # Continuation may lead to disatrous results.
            self.converge[:, jj] = self.converge[:, jj - 1]

        # Record the coefficients of normal/ annular Zernike polynomials after z4
        # in unit of nm
        self.zer4UpNm = self.converge[3:, jj] * 1e9

        # Status of iteration
        stopItr = False

        # Calculate the difference
        if (jj > 0):
            diffZk = np.sum(
                np.abs(self.converge[:, jj] - self.converge[:, jj - 1])) * 1e9

            # Check the Status of iteration
            if (diffZk < tol):
                stopItr = True

        # Update the current iteration time
        self.currentItr += 1

        # Show the Zk coefficients in interger in each iteration
        if (self.debugLevel >= 2):
            tmp = self.zer4UpNm
            print("itr = %d, z4-z%d" % (jj, self.parameter["numTerms"]))
            print(np.rint(tmp))

        return stopItr
Example #8
0
    def _genNormalizedFunc(self, r, theta, z, e):

        func = r * ZernikeAnnularEval(z, r * np.cos(theta), r * np.sin(theta), e) ** 2

        return func
Example #9
0
    def testZernikeAnnularEval(self):

        surface = ZernikeAnnularEval(self.zerCoef, self.xx, self.yy, self.obscuration)

        self._checkAnsWithFile(surface, "annularZernikeEval.txt")
Example #10
0
    def _singleItr(self, I1, I2, model, tol=1e-3):
        """Run the outer-loop with single iteration to solve the transport of
        intensity equation (TIE).

        This is to compensate the approximation of wavefront:
        S = -1/(delta Z) * (I1 - I2)/ (I1 + I2)).

        Parameters
        ----------
        I1 : CompensableImage
            Intra- or extra-focal image.
        I2 : CompensableImage
            Intra- or extra-focal image.
        model : str
            Optical model. It can be "paraxial", "onAxis", or "offAxis".
        tol : float, optional
            Tolerance of difference of coefficients of Zk polynomials compared
            with the previours iteration. (the default is 1e-3.)

        Returns
        -------
        bool
            Status of iteration.
        """

        # Use the zonal mode ("zer")
        compMode = self.getCompensatorMode()

        # Define the gain of feedbackGain
        feedbackGain = self.getFeedbackGain()

        # Set the pre-condition
        if self.currentItr == 0:

            # Check this is the first time of running iteration or not
            if I1.getImgInit() is None or I2.getImgInit() is None:

                # Check the image dimension
                if I1.getImg().shape != I2.getImg().shape:
                    print(
                        "Error: The intra and extra image stamps need to be of same size."
                    )
                    sys.exit()

                # Calculate the pupil mask (binary matrix) and related
                # parameters
                boundaryT = self.getBoundaryThickness()
                I1.makeMask(self._inst, model, boundaryT, 1)
                I2.makeMask(self._inst, model, boundaryT, 1)
                self._makeMasterMask(I1, I2, self.getPoissonSolverName())

                # Load the offAxis correction coefficients
                if model == "offAxis":
                    offAxisPolyOrder = self.getOffAxisPolyOrder()
                    I1.setOffAxisCorr(self._inst, offAxisPolyOrder)
                    I2.setOffAxisCorr(self._inst, offAxisPolyOrder)

                # Cocenter the images to the center referenced to fieldX and
                # fieldY. Need to check the availability of this.
                I1.imageCoCenter(self._inst, debugLevel=self.debugLevel)
                I2.imageCoCenter(self._inst, debugLevel=self.debugLevel)

                # Update the self-initial image
                I1.updateImgInit()
                I2.updateImgInit()

            # Initialize the variables used in the iteration.
            self.zcomp = np.zeros(self.getNumOfZernikes())
            self.zc = self.zcomp.copy()

            dimOfDonut = self._inst.getDimOfDonutOnSensor()
            self.wcomp = np.zeros((dimOfDonut, dimOfDonut))
            self.West = self.wcomp.copy()

            self.caustic = False

        # Rename this index (currentItr) for the simplification
        jj = self.currentItr

        # Solve the transport of intensity equation (TIE)
        if not self.caustic:

            # Reset the images before the compensation
            I1.updateImage(I1.getImgInit().copy())
            I2.updateImage(I2.getImgInit().copy())

            if compMode == "zer":

                # Zk coefficient from the previous iteration
                ztmp = self.zc.copy()

                # Do the feedback of Zk from the lower terms first based on the
                # sequence defined in compSequence
                if jj != 0:
                    compSequence = self.getCompSequence()
                    ztmp[int(compSequence[jj - 1]) :] = 0

                # Add partial feedback of residual estimated wavefront in Zk
                self.zcomp = self.zcomp + ztmp * feedbackGain

                # Remove the image distortion by forwarding the image to pupil
                I1.compensate(self._inst, self, self.zcomp, model)
                I2.compensate(self._inst, self, self.zcomp, model)

            # Check the image condition. If there is the problem, done with
            # this _singleItr().
            if (I1.isCaustic() is True) or (I2.isCaustic() is True):
                self.converge[:, jj] = self.converge[:, jj - 1]
                self.caustic = True
                return

            # Correct the defocal images if I1 and I2 are belong to different
            # sources, which is determined by the (fieldX, field Y)
            I1, I2 = self._applyI1I2pMask(I1, I2)

            # Solve the Poisson's equation
            self.zc, self.West = self._solvePoissonEq(I1, I2, jj)

            # Record/ calculate the Zk coefficient and wavefront
            if compMode == "zer":
                self.converge[:, jj] = self.zcomp + self.zc

                xoSensor, yoSensor = self._inst.getSensorCoorAnnular()
                self.wcomp = self.West + ZernikeAnnularEval(
                    np.concatenate(([0, 0, 0], self.zcomp[3:])),
                    xoSensor,
                    yoSensor,
                    self.getObsOfZernikes(),
                )

        else:
            # Once we run into caustic, stop here, results may be close to real
            # aberration.
            # Continuation may lead to disatrous results.
            self.converge[:, jj] = self.converge[:, jj - 1]

        # Record the coefficients of normal/ annular Zernike polynomials after
        # z4 in unit of nm
        self.zer4UpNm = self.converge[3:, jj] * 1e9

        # Status of iteration
        stopItr = False

        # Calculate the difference
        if jj > 0:
            diffZk = (
                np.sum(np.abs(self.converge[:, jj] - self.converge[:, jj - 1])) * 1e9
            )

            # Check the Status of iteration
            if diffZk < tol:
                stopItr = True

        # Update the current iteration time
        self.currentItr += 1

        # Show the Zk coefficients in interger in each iteration
        if self.debugLevel >= 2:
            print("itr = %d, z4-z%d" % (jj, self.getNumOfZernikes()))
            print(np.rint(self.zer4UpNm))

        return stopItr
Example #11
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
Example #12
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)