Ejemplo n.º 1
0
    def _getOffAxisCorrSingle(self, confFile):
        """Get the image-related pamameters for the off-axis distortion by the
        linear approximation with a series of fitted parameters with LSST
        ZEMAX model.

        Parameters
        ----------
        confFile : str
            Path of configuration file.

        Returns
        -------
        numpy.ndarray
            Coefficients for the off-axis distortion based on the linear
            response.
        float
            Defocal distance in m.
        """

        fieldDist = self._getFieldDistFromOrigin(minDist=0.0)

        # Read the configuration file
        paramReader = ParamReader()
        paramReader.setFilePath(confFile)
        cdata = paramReader.getMatContent()

        # Record the offset (defocal distance)
        offset = cdata[0, 0]

        # Take the reference parameters
        c = cdata[:, 1:]

        # Get the ruler, which is the distance to center
        # ruler is between 1.51 and 1.84 degree here
        ruler = np.sqrt(c[:, 0]**2 + c[:, 1]**2)

        # Get the fitted parameters for off-axis correction by linear
        # approximation
        corr_coeff = self._linearApprox(fieldDist, ruler, c[:, 2:])

        return corr_coeff, offset
Ejemplo n.º 2
0
    def _getOffAxisCorrSingle(self, confFile):
        """Get the image-related parameters for the off-axis distortion by the
        linear approximation with a series of fitted parameters with LSST
        ZEMAX model.

        Parameters
        ----------
        confFile : str
            Path of configuration file.

        Returns
        -------
        numpy.ndarray
            Coefficients for the off-axis distortion based on the linear
            response.
        float
            Defocal distance in m.
        """

        fieldDist = self._getFieldDistFromOrigin(minDist=0.0)

        # Read the configuration file
        paramReader = ParamReader()
        paramReader.setFilePath(confFile)
        cdata = paramReader.getMatContent()

        # Record the offset (defocal distance)
        offset = cdata[0, 0]

        # Take the reference parameters
        c = cdata[:, 1:]

        # Get the ruler, which is the distance to center
        # ruler is between 1.51 and 1.84 degree here
        ruler = np.sqrt(c[:, 0]**2 + c[:, 1]**2)

        # Get the fitted parameters for off-axis correction by linear
        # approximation
        corr_coeff = self._linearApprox(fieldDist, ruler, c[:, 2:])

        return corr_coeff, offset
Ejemplo n.º 3
0
class M1M3Sim(MirrorSim):
    def __init__(self):
        """Initiate the M1M3 simulator class."""

        # M2 setting file
        configDir = os.path.join(getConfigDir(), "M1M3")
        settingFilePath = os.path.join(configDir, "m1m3Setting.yaml")
        self._m1m3SettingFile = ParamReader(filePath=settingFilePath)

        # Inner and outer radius of M1 mirror in m
        radiusM1Inner = self._m1m3SettingFile.getSetting("radiusM1Inner")
        radiusM1Outer = self._m1m3SettingFile.getSetting("radiusM1Outer")

        # Inner and outer radius of M3 mirror in m
        radiusM3Inner = self._m1m3SettingFile.getSetting("radiusM3Inner")
        radiusM3Outer = self._m1m3SettingFile.getSetting("radiusM3Outer")

        super(M1M3Sim,
              self).__init__((radiusM1Inner, radiusM3Inner),
                             (radiusM1Outer, radiusM3Outer), configDir)

        # Mirror surface bending mode grid file
        self._gridFile = ParamReader()

        # FEA model file
        self._feaFile = ParamReader()

        # FEA model data in zenith angle
        self._feaZenFile = ParamReader()

        # FEA model data in horizontal angle
        self._feaHorFile = ParamReader()

        # Actuator forces along zenith direction
        self._forceZenFile = ParamReader()

        # Actuator forces along horizon direction
        self._forceHorFile = ParamReader()

        # Influence matrix of actuator forces
        self._forceInflFile = ParamReader()

        self._config("M1M3_1um_156_force.yaml", "M1M3_LUT.yaml",
                     "M1M3_1um_156_grid.yaml", "M1M3_thermal_FEA.yaml",
                     "M1M3_dxdydz_zenith.yaml", "M1M3_dxdydz_horizon.yaml",
                     "M1M3_force_zenith.yaml", "M1M3_force_horizon.yaml",
                     "M1M3_influence_256.yaml")

    def _config(self, actForceFileName, lutFileName, gridFileName, feaFileName,
                feaZenFileName, feaHorFileName, forceZenFileName,
                forceHorFileName, forceInflFileName):
        """Do the configuration.

        LUT: Look-up table.
        FEA: Finite element analysis.

        Parameters
        ----------
        actForceFileName : str
            Actuator force file name.
        lutFileName : str
            LUT file name.
        gridFileName : str
            File name of bending mode data.
        feaFileName : str
            FEA model data file name.
        feaZenFileName : str
            FEA model data file name in zenith angle.
        feaHorFileName : str
            FEA model data file name in horizontal angle.
        forceZenFileName : str
            File name of actuator forces along zenith direction.
        forceHorFileName : str
            File name of actuator forces along horizon direction.
        forceInflFileName : str
            Influence matrix of actuator forces.
        """

        numTerms = self._m1m3SettingFile.getSetting("numTerms")

        super(M1M3Sim, self).config(numTerms=numTerms,
                                    actForceFileName=actForceFileName,
                                    lutFileName=lutFileName)

        mirrorDataDir = self.getMirrorDataDir()

        gridFilePath = os.path.join(mirrorDataDir, gridFileName)
        self._gridFile.setFilePath(gridFilePath)

        feaFilePath = os.path.join(mirrorDataDir, feaFileName)
        self._feaFile.setFilePath(feaFilePath)

        feaZenFilePath = os.path.join(mirrorDataDir, feaZenFileName)
        self._feaZenFile.setFilePath(feaZenFilePath)

        feaHorFilePath = os.path.join(mirrorDataDir, feaHorFileName)
        self._feaHorFile.setFilePath(feaHorFilePath)

        forceZenFilePath = os.path.join(mirrorDataDir, forceZenFileName)
        self._forceZenFile.setFilePath(forceZenFilePath)

        forceHorFilePath = os.path.join(mirrorDataDir, forceHorFileName)
        self._forceHorFile.setFilePath(forceHorFilePath)

        forceInflFilePath = os.path.join(mirrorDataDir, forceInflFileName)
        self._forceInflFile.setFilePath(forceInflFilePath)

    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

    def _getMirCoor(self):
        """Get the mirror coordinate and node.

        Returns
        -------
        numpy.ndarray[int]
            M1 node.
        numpy.ndarray[int]
            M3 node.
        numpy.ndarray
            x coordinate.
        numpy.ndarray
            y coordinate.
        numpy.ndarray
            z coordinate.
        """

        # Get the bending mode information
        data = self._gridFile.getMatContent()

        nodeID = data[:, 0].astype("int")
        nodeM1 = (nodeID == 1)
        nodeM3 = (nodeID == 3)

        bx = data[:, 1]
        by = data[:, 2]
        bz = data[:, 3:]

        return nodeM1, nodeM3, bx, by, bz

    def _calcIdealShape(self,
                        xInMm,
                        yInMm,
                        idxM1,
                        idxM3,
                        dr1=0,
                        dr3=0,
                        dk1=0,
                        dk3=0):
        """Calculate the ideal shape of mirror along z direction.

        This is described by a series of cylindrically-symmetric aspheric
        surfaces.

        Parameters
        ----------
        xInMm : numpy.ndarray
            Coordinate x in 1D array in mm.
        yInMm : numpy.ndarray
            Coordinate y in 1D array in mm.
        idxM1 : numpy.ndarray [int]
            M1 node.
        idxM3 : numpy.ndarray [int]
            M3 node.
        dr1 : float, optional
            Displacement of r in mirror 1. (the default is 0.)
        dr3 : float, optional
            Displacement of r in mirror 3. (the default is 0.)
        dk1 : float, optional
            Displacement of kappa (k) in mirror 1. (the default is 0.)
        dk3 : float, optional
            Displacement of kappa (k) in mirror 3. (the default is 0.)

        Returns
        -------
        numpy.ndarray
            Ideal mirror surface along z direction.

        Raises
        ------
        ValueError
            X is unequal to y.
        """

        # M1 optical design
        r1 = -1.9835e4
        k1 = -1.215
        alpha1 = np.zeros((8, 1))
        alpha1[2] = 1.38e-24

        # M3 optical design
        r3 = -8344.5
        k3 = 0.155
        alpha3 = np.zeros((8, 1))
        alpha3[2] = -4.5e-22
        alpha3[3] = -8.15e-30

        # Get the dimension of input xInMm, yInMm
        nr = xInMm.shape
        mr = yInMm.shape
        if (nr != mr):
            raise ValueError("X[%d] is unequal to y[%d]." % (nr, mr))

        # Calculation the curvature (c) and conic constant (kappa)

        # Mirror 1 (M1)
        c1 = 1 / (r1 + dr1)
        k1 = k1 + dk1

        # Mirror 3 (M3)
        c3 = 1 / (r3 + dr3)
        k3 = k3 + dk3

        # Construct the curvature, kappa, and alpha matrixes for the ideal
        # shape calculation
        cMat = np.zeros(nr)
        cMat[idxM1] = c1
        cMat[idxM3] = c3

        kMat = np.zeros(nr)
        kMat[idxM1] = k1
        kMat[idxM3] = k3

        alphaMat = np.tile(np.zeros(nr), (8, 1))
        for ii in range(8):
            alphaMat[ii, idxM1] = alpha1[ii]
            alphaMat[ii, idxM3] = alpha3[ii]

        # Calculate the radius
        r2 = xInMm**2 + yInMm**2

        # Calculate the ideal surface

        # The optical elements of telescopes can often be described by a
        # series of cylindrically-symmetric aspheric surfaces:
        # z(r) = c * r^2/[ 1 + sqrt( 1-(1+k) * c^2 * r^2 ) ] +
        # sum(ai * r^(2*i)) + sum(Aj * Zj)
        # where i = 1-8, j = 1-N

        z0 = cMat * r2 / (1 + np.sqrt(1 - (1 + kMat) * cMat**2 * r2))
        for ii in range(8):
            z0 += alphaMat[ii, :] * r2**(ii + 1)

        # M3 vertex offset from M1 vertex, values from Zemax model
        # M3voffset = (233.8 - 233.8 - 900 - 3910.701 - 1345.500 + 1725.701
        # + 3530.500 + 900 + 233.800)
        M3voffset = 233.8

        # Add the M3 offset (sum(Aj * Zj), j = 1 - N)
        z0[idxM3] = z0[idxM3] + M3voffset

        # In Zemax, z axis points from M1M3 to M2. the reversed direction
        # (z0>0) is needed. That means the direction of M2 to M1M3.
        return -z0

    def getTempCorr(self, m1m3TBulk, m1m3TxGrad, m1m3TyGrad, m1m3TzGrad,
                    m1m3TrGrad):
        """Get the mirror print correction along z direction for certain
        temperature gradient.

        FEA: Finite element analysis.

        Parameters
        ----------
        m1m3TBulk : float
            Bulk temperature in degree C (+/-2sigma spans +/-0.8C).
        m1m3TxGrad : float
            Temperature gradient along x direction in degree C (+/-2sigma
            spans 0.4C).
        m1m3TyGrad : float
            Temperature gradient along y direction in degree C (+/-2sigma
            spans 0.4C).
        m1m3TzGrad : float
            Temperature gradient along z direction in degree C (+/-2sigma
            spans 0.1C).
        m1m3TrGrad : float
            Temperature gradient along r direction in degree C (+/-2sigma
            spans 0.1C).

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

        # Data needed to determine thermal deformation
        data = self._feaFile.getMatContent()

        # These are the normalized coordinates

        # In the original XLS file (thermal FEA model), max(x)=164.6060 in,
        # while 4.18m = 164.5669 in for real mirror. These two numbers do
        # not match.
        # The data here has normalized the dimension (x, y) already.
        # n.b. these may not have been normalized correctly, b/c max(tx)=1.0
        tx = data[:, 0]
        ty = data[:, 1]

        # Below are in M1M3 coordinate system, and in micron

        # Do the fitting in the normalized coordinate
        bx, by = self._getMirCoor()[2:4]
        R = self.getOuterRinM()[0]
        normX = bx / R
        normY = by / R

        # Fit the bulk
        tbdz = self._fitData(tx, ty, data[:, 2], normX, normY)

        # Fit the x-grad
        txdz = self._fitData(tx, ty, data[:, 3], normX, normY)

        # Fit the y-grad
        tydz = self._fitData(tx, ty, data[:, 4], normX, normY)

        # Fit the z-grad
        tzdz = self._fitData(tx, ty, data[:, 5], normX, normY)

        # Fit the r-gradß
        trdz = self._fitData(tx, ty, data[:, 6], normX, normY)

        # Get the temprature correction
        tempCorrInUm = m1m3TBulk*tbdz + m1m3TxGrad*txdz + m1m3TyGrad*tydz + \
            m1m3TzGrad*tzdz + m1m3TrGrad*trdz

        return tempCorrInUm

    def _fitData(self, dataX, dataY, data, x, y):
        """Fit the data by radial basis function.

        Parameters
        ----------
        dataX : numpy.ndarray
            Data x.
        dataY : numpy.ndarray
            Data y.
        data : numpy.ndarray
            Data to fit.
        x : numpy.ndarray
            x coordinate.
        y : numpy.ndarray
            y coordinate.

        Returns
        -------
        numpy.ndarray
            Fitted data.
        """

        # Construct the fitting model
        rbfi = Rbf(dataX, dataY, data)

        # Return the fitted data
        return rbfi(x, y)

    def getMirrorResInMmInZemax(self, writeZcInMnToFilePath=None):
        """Get the residue of surface (mirror print along z-axis) in mm under
        the Zemax coordinate.

        This value is after the fitting with spherical Zernike polynomials
        (zk).

        Parameters
        ----------
        writeZcInMnToFilePath : str, optional
            File path to write the fitted zk in mm. (the default is None.)

        Returns
        ------
        numpy.ndarray
            Fitted residue in mm after removing the fitted zk terms in Zemax
            coordinate.
        numpy.ndarray
            X position in mm in Zemax coordinate.
        numpy.ndarray
            Y position in mm in Zemax coordinate.
        numpy.ndarray
            Fitted zk in mm in Zemax coordinate.
        """

        # Get the bending mode information
        bx, by, bz = self._getMirCoor()[2:5]

        # Transform the M1M3 coordinate to Zemax coordinate
        bxInZemax, byInZemax, surfInZemax = opt2ZemaxCoorTrans(
            bx, by, self.getSurfAlongZ())

        # Get the mirror residue and zk in um
        RinM = self.getOuterRinM()[0]
        resInUmInZemax, zcInUmInZemax = self._getMirrorResInNormalizedCoor(
            surfInZemax, bxInZemax / RinM, byInZemax / RinM)

        # Change the unit to mm
        resInMmInZemax = resInUmInZemax * 1e-3
        bxInMmInZemax = bxInZemax * 1e3
        byInMmInZemax = byInZemax * 1e3
        zcInMmInZemax = zcInUmInZemax * 1e-3

        # Save the file of fitted Zk
        if (writeZcInMnToFilePath is not None):
            np.savetxt(writeZcInMnToFilePath, zcInMmInZemax)

        return resInMmInZemax, bxInMmInZemax, byInMmInZemax, zcInMmInZemax

    def writeMirZkAndGridResInZemax(self,
                                    resFile=[],
                                    surfaceGridN=200,
                                    writeZcInMnToFilePath=None):
        """Write the grid residue in mm of mirror surface after the fitting
        with Zk under the Zemax coordinate.

        Parameters
        ----------
        resFile : list, optional
            File path to save the grid surface residue map. (the default
            is [].)
        surfaceGridN : {number}, optional
            Surface grid number. (the default is 200.)
        writeZcInMnToFilePath : str, optional
            File path to write the fitted zk in mm. (the default is None.)

        Returns
        -------
        str
            Grid residue map related data of M1.
        str
            Grid residue map related data of M3.
        """

        # Get the residure map
        resInMmInZemax, bxInMmInZemax, byInMmInZemax = \
            self.getMirrorResInMmInZemax(
                writeZcInMnToFilePath=writeZcInMnToFilePath)[0:3]

        # Get the mirror node
        idx1, idx3 = self._getMirCoor()[0:2]

        # Grid sample map for M1 and M3
        for ii, idx in zip((0, 1), (idx1, idx3)):

            # Change the unit from m to mm
            innerRinMm = self.getInnerRinM()[ii] * 1e3
            outerRinMm = self.getOuterRinM()[ii] * 1e3

            # Get the residue map used in Zemax
            # Content header: (NUM_X_PIXELS, NUM_Y_PIXELS, delta x, delta y)
            # Content: (z, dx, dy, dxdy)
            content = self._gridSampInMnInZemax(resInMmInZemax[idx],
                                                bxInMmInZemax[idx],
                                                byInMmInZemax[idx],
                                                innerRinMm,
                                                outerRinMm,
                                                surfaceGridN,
                                                surfaceGridN,
                                                resFile=resFile[ii])
            if (ii == 0):
                contentM1 = content
            elif (ii == 1):
                contentM3 = content

        return contentM1, contentM3

    def showMirResMap(self, resFile, writeToResMapFilePath=[]):
        """Show the mirror residue map.

        Parameters
        ----------
        resFile : list
            File path of the grid surface residue map.
        writeToResMapFilePath : list, optional
            File path to save the residue map. (the default is [].)
        """

        # Get the residure map
        resInMmInZemax, bxInMmInZemax, byInMmInZemax = \
            self.getMirrorResInMmInZemax()[0:3]

        # Get the mirror node
        idx1, idx3 = self._getMirCoor()[0:2]

        # Show the mirror maps
        RinMtuple = self.getOuterRinM()

        for ii, RinM, idx in zip((0, 1), RinMtuple, (idx1, idx3)):
            outerRinMm = RinM * 1e3
            plotResMap(resInMmInZemax[idx],
                       bxInMmInZemax[idx],
                       byInMmInZemax[idx],
                       outerRinMm,
                       resFile=resFile[ii],
                       writeToResMapFilePath=writeToResMapFilePath[ii])

    def genMirSurfRandErr(self,
                          zAngleInRadian,
                          m1m3ForceError=0.05,
                          seedNum=0):
        """Generate the mirror surface random error.

        LUT: Loop-up table.

        Parameters
        ----------
        zAngleInRadian : float
            Zenith angle in radian.
        m1m3ForceError : float, optional
            Ratio of actuator force error. (the default is 0.05.)
        seedNum : int, optional
            Random seed number. (the default is 0.)

        Returns
        -------
        numpy.ndarray
            Generated mirror surface random error in m.
        """

        # Get the actuator forces in N of M1M3 based on the look-up table (LUT)
        zangleInDeg = np.rad2deg(zAngleInRadian)
        LUTforce = self.getLUTforce(zangleInDeg)

        # Assume the m1m3ForceError=0.05
        # Add 5% force error to the original actuator forces
        # This means from -5% to +5% of original actuator's force.
        np.random.seed(int(seedNum))
        nActuator = len(LUTforce)
        myu = (1 + 2 *
               (np.random.rand(nActuator) - 0.5) * m1m3ForceError) * LUTforce

        # Balance forces along z-axis
        # This statement is intentionally to make the force balance.
        nzActuator = int(self._m1m3SettingFile.getSetting("numActuatorInZ"))
        myu[nzActuator-1] = np.sum(LUTforce[:nzActuator]) - \
            np.sum(myu[:nzActuator-1])

        # Balance forces along y-axis
        # This statement is intentionally to make the force balance.
        myu[nActuator-1] = np.sum(LUTforce[nzActuator:]) - \
            np.sum(myu[nzActuator:-1])

        # Get the net force along the z-axis
        zf = self._forceZenFile.getMatContent()
        hf = self._forceHorFile.getMatContent()
        u0 = zf * np.cos(zAngleInRadian) + hf * np.sin(zAngleInRadian)

        # Calculate the random surface
        G = self._forceInflFile.getMatContent()
        randSurfInM = G.dot(myu - u0)

        return randSurfInM
Ejemplo n.º 4
0
class Algorithm(object):

    def __init__(self, algoDir):
        """Initialize the Algorithm class.

        Algorithm used to solve the transport of intensity equation to get
        normal/ annular Zernike polynomials.

        Parameters
        ----------
        algoDir : str
            Algorithm configuration directory.
        """

        self.algoDir = algoDir
        self.algoParamFile = ParamReader()

        self._inst = Instrument("")

        # Show the calculation message based on this value
        # 0 means no message will be showed
        self.debugLevel = 0

        # Image has the problem or not from the over-compensation
        self.caustic = False

        # Record the Zk coefficients in each outer-loop iteration
        # The actual total outer-loop iteration time is Num_of_outer_itr + 1
        self.converge = np.array([])

        # Current number of outer-loop iteration
        self.currentItr = 0

        # Record the coefficients of normal/ annular Zernike polynomials after
        # z4 in unit of nm
        self.zer4UpNm = np.array([])

        # Converged wavefront.
        self.wcomp = np.array([])

        # Calculated wavefront in previous outer-loop iteration.
        self.West = np.array([])

        # Converged Zk coefficients
        self.zcomp = np.array([])

        # Calculated Zk coefficients in previous outer-loop iteration
        self.zc = np.array([])

        # Padded mask for use at the offset planes
        self.pMask = None

        # Non-padded mask corresponding to aperture
        self.cMask = None

        # Change the dimension of mask for fft to use
        self.pMaskPad = None
        self.cMaskPad = None

    def reset(self):
        """Reset the calculation for the new input images with the same
        algorithm settings."""

        self.caustic = False
        self.converge = np.zeros(self.converge.shape)
        self.currentItr = 0
        self.zer4UpNm = np.zeros(self.zer4UpNm.shape)

        self.wcomp = np.zeros(self.wcomp.shape)
        self.West = np.zeros(self.West.shape)

        self.zcomp = np.zeros(self.zcomp.shape)
        self.zc = np.zeros(self.zc.shape)

        self.pMask = None
        self.cMask = None

        self.pMaskPad = None
        self.cMaskPad = None

    def config(self, algoName, inst, debugLevel=0):
        """Configure the algorithm to solve TIE.

        Parameters
        ----------
        algoName : str
            Algorithm configuration file to solve the Poisson's equation in the
            transport of intensity equation (TIE). It can be "fft" or "exp"
            here.
        inst : Instrument
            Instrument to use.
        debugLevel : int, optional
            Show the information under the running. If the value is higher, the
            information shows more. It can be 0, 1, 2, or 3. (the default is
            0.)
        """

        algoParamFilePath = os.path.join(self.algoDir, "%s.yaml" % algoName)
        self.algoParamFile.setFilePath(algoParamFilePath)

        self._inst = inst
        self.debugLevel = debugLevel

        self.caustic = False

        numTerms = self.getNumOfZernikes()
        outerItr = self.getNumOfOuterItr()
        self.converge = np.zeros((numTerms, outerItr + 1))

        self.currentItr = 0

        self.zer4UpNm = np.zeros(numTerms - 3)

        # Wavefront related parameters
        dimOfDonut = self._inst.getDimOfDonutOnSensor()
        self.wcomp = np.zeros((dimOfDonut, dimOfDonut))
        self.West = self.wcomp.copy()

        # Used in model basis ("zer").
        self.zcomp = np.zeros(numTerms)
        self.zc = self.zcomp.copy()

        # Mask related variables
        self.pMask = None
        self.cMask = None
        self.pMaskPad = None
        self.cMaskPad = None

    def setDebugLevel(self, debugLevel):
        """Set the debug level.

        If the value is higher, the information shows more. It can be 0, 1, 2,
        or 3.

        Parameters
        ----------
        debugLevel : int
            Show the information under the running.
        """

        self.debugLevel = int(debugLevel)

    def getDebugLevel(self):
        """Get the debug level.

        If the value is higher, the information shows more. It can be 0, 1, 2,
        or 3.

        Returns
        -------
        int
            Debug level.
        """

        return self.debugLevel

    def getZer4UpInNm(self):
        """Get the coefficients of Zernike polynomials of z4-zn in nm.

        Returns
        -------
        numpy.ndarray
            Zernike polynomials of z4-zn in nm.
        """

        return self.zer4UpNm

    def getPoissonSolverName(self):
        """Get the method name to solve the Poisson equation.

        Returns
        -------
        str
            Method name to solve the Poisson equation.
        """

        return self.algoParamFile.getSetting("poissonSolver")

    def getNumOfZernikes(self):
        """Get the maximum number of Zernike polynomials supported.

        Returns
        -------
        int
            Maximum number of Zernike polynomials supported.
        """

        return int(self.algoParamFile.getSetting("numOfZernikes"))

    def getZernikeTerms(self):
        """Get the Zernike terms in using.

        Returns
        -------
        numpy.ndarray
            Zernkie terms in using.
        """

        numTerms = self.getNumOfZernikes()
        zTerms = np.arange(numTerms) + 1

        return zTerms

    def getObsOfZernikes(self):
        """Get the obscuration of annular Zernike polynomials.

        Returns
        -------
        float
            Obscuration of annular Zernike polynomials
        """

        zobsR = self.algoParamFile.getSetting("obsOfZernikes")
        if (zobsR == 1):
            zobsR = self._inst.getObscuration()

        return float(zobsR)

    def getNumOfOuterItr(self):
        """Get the number of outer loop iteration.

        Returns
        -------
        int
            Number of outer loop iteration.
        """

        return int(self.algoParamFile.getSetting("numOfOuterItr"))

    def getNumOfInnerItr(self):
        """Get the number of inner loop iteration.

        This is for the fast Fourier transform (FFT) solver only.

        Returns
        -------
        int
            Number of inner loop iteration.
        """

        return int(self.algoParamFile.getSetting("numOfInnerItr"))

    def getFeedbackGain(self):
        """Get the gain value used in the outer loop iteration.

        Returns
        -------
        float
            Gain value used in the outer loop iteration.
        """

        return self.algoParamFile.getSetting("feedbackGain")

    def getOffAxisPolyOrder(self):
        """Get the number of polynomial order supported in off-axis correction.

        Returns
        -------
        int
            Number of polynomial order supported in off-axis correction.
        """

        return int(self.algoParamFile.getSetting("offAxisPolyOrder"))

    def getCompensatorMode(self):
        """Get the method name to compensate the wavefront by wavefront error.

        Returns
        -------
        str
            Method name to compensate the wavefront by wavefront error.
        """

        return self.algoParamFile.getSetting("compensatorMode")

    def getCompSequence(self):
        """Get the compensated sequence of Zernike order for each iteration.

        Returns
        -------
        numpy.ndarray[int]
            Compensated sequence of Zernike order for each iteration.
        """

        compSequenceFromFile = self.algoParamFile.getSetting("compSequence")
        compSequence = np.array(compSequenceFromFile, dtype=int)

        # If outerItr is large, and compSequence is too small,
        # the rest in compSequence will be filled.
        # This is used in the "zer" method.
        outerItr = self.getNumOfOuterItr()
        compSequence = self._extend1dArray(compSequence, outerItr)
        compSequence = compSequence.astype(int)

        return compSequence

    def _extend1dArray(self, origArray, targetLength):
        """Extend the 1D original array to the taget length.

        The extended value will be the final element of original array. Nothing
        will be done if the input array is not 1D or its length is less than
        the target.

        Parameters
        ----------
        origArray : numpy.ndarray
            Original array with 1 dimension.
        targetLength : int
            Target length of new extended array.

        Returns
        -------
        numpy.ndarray
            Extended 1D array.
        """

        if (len(origArray) < targetLength) and (origArray.ndim == 1):
            leftOver = np.ones(targetLength - len(origArray))
            extendArray = np.append(origArray, origArray[-1] * leftOver)
        else:
            extendArray = origArray

        return extendArray

    def getBoundaryThickness(self):
        """Get the boundary thickness that the computation mask extends beyond
        the pupil mask.

        It is noted that in Fast Fourier transform (FFT) algorithm, it is also
        the width of Neuman boundary where the derivative of the wavefront is
        set to zero

        Returns
        -------
        int
            Boundary thickness.
        """

        return int(self.algoParamFile.getSetting("boundaryThickness"))

    def getFftDimension(self):
        """Get the FFT pad dimension in pixel.

        This is for the fast Fourier transform (FFT) solver only.

        Returns
        -------
        int
            FFT pad dimention.
        """

        fftDim = int(self.algoParamFile.getSetting("fftDimension"))

        # Make sure the dimension is the order of multiple of 2
        if (fftDim == 999):
            dimToFit = self._inst.getDimOfDonutOnSensor()
        else:
            dimToFit = fftDim

        padDim = int(2**np.ceil(np.log2(dimToFit)))

        return padDim

    def getSignalClipSequence(self):
        """Get the signal clip sequence.

        The number of values should be the number of compensation plus 1.
        This is for the fast Fourier transform (FFT) solver only.

        Returns
        -------
        numpy.ndarray
            Signal clip sequence.
        """

        sumclipSequenceFromFile = self.algoParamFile.getSetting("signalClipSequence")
        sumclipSequence = np.array(sumclipSequenceFromFile)

        # If outerItr is large, and sumclipSequence is too small, the rest in
        # sumclipSequence will be filled.
        # This is used in the "zer" method.
        targetLength = self.getNumOfOuterItr() + 1
        sumclipSequence = self._extend1dArray(sumclipSequence, targetLength)

        return sumclipSequence

    def getMaskScalingFactor(self):
        """Get the mask scaling factor for fast beam.

        Returns
        -------
        float
            Mask scaling factor for fast beam.
        """

        # m = R'*f/(l*R), R': radius of the no-aberration image
        focalLength = self._inst.getFocalLength()
        marginalFL = self._inst.getMarginalFocalLength()
        maskScalingFactor = focalLength / marginalFL

        return maskScalingFactor

    def itr0(self, I1, I2, model):
        """Calculate the wavefront and coefficients of normal/ annular Zernike
        polynomials in the first iteration time.

        Parameters
        ----------
        I1 : Image
            Intra- or extra-focal image.
        I2 : Image
            Intra- or extra-focal image.
        model : str
            Optical model. It can be "paraxial", "onAxis", or "offAxis".
        """

        # Reset the iteration time of outer loop and decide to reset the
        # defocal images or not
        self._reset(I1, I2)

        # Solve the transport of intensity equation (TIE)
        self._singleItr(I1, I2, model)

    def runIt(self, I1, I2, model, tol=1e-3):
        """Calculate the wavefront error by solving the transport of intensity
        equation (TIE).

        The inner (for fft algorithm) and outer loops are used. The inner loop
        is to solve the Poisson's equation. The outer loop is to compensate the
        intra- and extra-focal images to mitigate the calculation of wavefront
        (e.g. S = -1/(delta Z) * (I1 - I2)/ (I1 + I2)).

        Parameters
        ----------
        I1 : Image
            Intra- or extra-focal image.
        I2 : Image
            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.)
        """

        # To have the iteration time initiated from global variable is to
        # distinguish the manually and automatically iteration processes.
        itr = self.currentItr
        while (itr <= self.getNumOfOuterItr()):
            stopItr = self._singleItr(I1, I2, model, tol)

            # Stop the iteration of outer loop if converged
            if (stopItr):
                break

            itr += 1

    def nextItr(self, I1, I2, model, nItr=1):
        """Run the outer loop iteration with the specific time defined in nItr.

        Parameters
        ----------
        I1 : Image
            Intra- or extra-focal image.
        I2 : Image
            Intra- or extra-focal image.
        model : str
            Optical model. It can be "paraxial", "onAxis", or "offAxis".
        nItr : int, optional
            Outer loop iteration time. (the default is 1.)
        """

        #  Do the iteration
        ii = 0
        while (ii < nItr):
            self._singleItr(I1, I2, model)
            ii += 1

    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 : Image
            Intra- or extra-focal image.
        I2 : Image
            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

                # 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 if the optical model is not
                # "paraxial"
                # Only the optical model of "onAxis" or "offAxis" is considered
                # here
                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):
            tmp = self.zer4UpNm
            print("itr = %d, z4-z%d" % (jj, self.getNumOfZernikes()))
            print(np.rint(tmp))

        return stopItr

    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

    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

    def _getdIandI(self, I1, I2):
        """Calculate the central image and differential image to be used in the
        serial expansion method.

        It is noted that the images are assumed to be co-center already. And
        the intra-/ extra-focal image can overlap with one another after the
        rotation of 180 degree.

        Parameters
        ----------
        I1 : Image
            Intra- or extra-focal image.
        I2 : Image
            Intra- or extra-focal image.

        Returns
        -------
        numpy.ndarray
            Image data of I0.
        numpy.ndarray
            Differential image (dI) of I0.
        """

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

        # Calculate the central image and differential iamge
        I0 = (I1image+I2image)/2
        dI = I2image-I1image

        return I0, dI

    def _checkImageDim(self, I1, I2):
        """Check the dimension of images.

        It is noted that the I2 image is rotated by 180 degree.

        Parameters
        ----------
        I1 : Image
            Intra- or extra-focal image.
        I2 : Image
            Intra- or extra-focal image.

        Returns
        -------
        numpy.ndarray
            I1 defocal image.
        numpy.ndarray
            I2 defocal image. It is noted that the I2 image is rotated by 180
            degree.

        Raises
        ------
        Exception
            Check the dimension of images is n by n or not.
        Exception
            Check two defocal images have the same size or not.
        """

        # Check the condition of images
        m1, n1 = I1.getImg().shape
        m2, n2 = I2.getImg().shape

        if (m1 != n1 or m2 != n2):
            raise Exception("Image is not square.")

        if (m1 != m2 or n1 != n2):
            raise Exception("Images do not have the same size.")

        # Define I1
        I1image = I1.getImg()

        # Rotate the image by 180 degree through rotating two times of 90
        # degree
        I2image = np.rot90(I2.getImg(), k=2)

        return I1image, I2image

    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)

    def _applyI1I2pMask(self, I1, I2):
        """Correct the defocal images if I1 and I2 are belong to different
        sources.

        (There is a problem for this actually. If I1 and I2 come from different
        sources, what should the correction of TIE be? At this moment, the
        fieldX and fieldY of I1 and I2 should be different. And the sources are
        different also.)

        Parameters
        ----------
        I1 : Image
            Intra- or extra-focal image.
        I2 : Image
            Intra- or extra-focal image.

        Returns
        -------
        numpy.ndarray
            Corrected I1 image.
        numpy.ndarray
            Corrected I2 image.
        """

        # Get the overlap region of images and do the normalization.
        if (I1.fieldX != I2.fieldX or I1.fieldY != I2.fieldY):

            # Get the overlap region of image
            I1.updateImage(I1.getImg()*self.pMask)

            # Rotate the image by 180 degree through rotating two times of 90
            # degree
            I2.updateImage(I2.getImg()*np.rot90(self.pMask, 2))

            # Do the normalization of image.
            I1.updateImage(I1.getImg()/np.sum(I1.getImg()))
            I2.updateImage(I2.getImg()/np.sum(I2.getImg()))

        # Return the correct images. It is noted that there is no need of
        # vignetting correction.
        # This is after masking already in _singleItr() or itr0().
        return I1, I2

    def _reset(self, I1, I2):
        """Reset the iteration time of outer loop and defocal images.

        Parameters
        ----------
        I1 : Image
            Intra- or extra-focal image.
        I2 : Image
            Intra- or extra-focal image.
        """

        # Reset the current iteration time to 0
        self.currentItr = 0

        # Show the reset information
        if (self.debugLevel >= 3):
            print("Resetting images: I1 and I2")

        # Determine to reset the images or not based on the existence of
        # the attribute: Image.image0. Only after the first run of
        # inner loop, this attribute will exist.
        try:
            # Reset the images to the first beginning
            I1.updateImage(I1.getImgInit().copy())
            I2.updateImage(I2.getImgInit().copy())

            # Show the information of resetting image
            if (self.debugLevel >= 3):
                print("Resetting images in inside.")

        except AttributeError:
            # Show the information of no image0
            if (self.debugLevel >= 3):
                print("Image0 = None. This is the first time to run the code.")

            pass

    def outZer4Up(self, unit="nm", filename=None, showPlot=False):
        """Put the coefficients of normal/ annular Zernike polynomials on
        terminal or file ande show the image if it is needed.

        Parameters
        ----------
        unit : str, optional
            Unit of the coefficients of normal/ annular Zernike polynomials. It
            can be m, nm, or um. (the default is "nm".)
        filename : str, optional
            Name of output file. (the default is None.)
        showPlot : bool, optional
            Decide to show the plot or not. (the default is False.)
        """

        # List of Zn,m
        Znm = ["Z0,0", "Z1,1", "Z1,-1", "Z2,0", "Z2,-2", "Z2,2", "Z3,-1",
               "Z3,1", "Z3,-3", "Z3,3", "Z4,0", "Z4,2", "Z4,-2", "Z4,4",
               "Z4,-4", "Z5,1", "Z5,-1", "Z5,3", "Z5,-3", "Z5,5", "Z5,-5",
               "Z6,0"]

        # Decide the format of z based on the input unit (m, nm, or um)
        if (unit == "m"):
            z = self.zer4UpNm*1e-9
        elif (unit == "nm"):
            z = self.zer4UpNm
        elif (unit == "um"):
            z = self.zer4UpNm*1e-3
        else:
            print("Unknown unit: %s" % unit)
            print("Unit options are: m, nm, um")
            return

        # Write the coefficients into a file if needed.
        if (filename is not None):
            f = open(filename, "w")
        else:
            f = sys.stdout

        for ii in range(4, len(z)+4):
            f.write("Z%d (%s)\t %8.3f\n" % (ii, Znm[ii-1], z[ii-4]))

        # Close the file
        if (filename is not None):
            f.close()

        # Show the plot
        if (showPlot):
            plt.figure()

            x = range(4, len(z) + 4)
            plt.plot(x, z, marker="o", color="r", markersize=10)
            plt.xlabel("Zernike Index")
            plt.ylabel("Zernike coefficient (%s)" % unit)
            plt.grid()
            plt.show()
Ejemplo n.º 5
0
class TestParamReader(unittest.TestCase):
    """Test the ParamReaderYaml class."""

    def setUp(self):

        testDir = os.path.join(getModulePath(), "tests")
        self.configDir = os.path.join(testDir, "testData")
        self.fileName = "testConfigFile.yaml"

        filePath = os.path.join(self.configDir, self.fileName)
        self.paramReader = ParamReader(filePath=filePath)

        self.testTempDir = tempfile.TemporaryDirectory(dir=testDir)

    def tearDown(self):

        self.testTempDir.cleanup()

    def testGetSetting(self):

        znmax = self.paramReader.getSetting("znmax")
        self.assertEqual(znmax, 22)

    def testGetSettingWithWrongParam(self):

        self.assertRaises(ValueError, self.paramReader.getSetting, "wrongParam")

    def testGetFilePath(self):

        ansFilePath = os.path.join(self.configDir, self.fileName)
        self.assertEqual(self.paramReader.getFilePath(), ansFilePath)

    def testSetFilePath(self):

        fileName = "test.yaml"
        filePath = os.path.join(self.configDir, fileName)
        with self.assertWarns(UserWarning):
            self.paramReader.setFilePath(filePath)

        self.assertEqual(self.paramReader.getFilePath(), filePath)

    def testGetContent(self):

        content = self.paramReader.getContent()
        self.assertTrue(isinstance(content, dict))

    def testGetContentWithDefaultSetting(self):

        paramReader = ParamReader()

        content = paramReader.getContent()
        self.assertTrue(isinstance(content, dict))

    def testWriteMatToFile(self):

        self._writeMatToFile()

        numOfFile = self._getNumOfFileInFolder(self.testTempDir.name)
        self.assertEqual(numOfFile, 1)

    def _writeMatToFile(self):

        mat = np.random.rand(3, 4, 5)
        filePath = os.path.join(self.testTempDir.name, "temp.yaml")
        ParamReader.writeMatToFile(mat, filePath)

        return mat, filePath

    def _getNumOfFileInFolder(self, folder):

        return len(
            [
                name
                for name in os.listdir(folder)
                if os.path.isfile(os.path.join(folder, name))
            ]
        )

    def testWriteMatToFileWithWrongFileFormat(self):

        wrongFilePath = os.path.join(self.testTempDir.name, "temp.txt")
        self.assertRaises(
            ValueError, ParamReader.writeMatToFile, np.ones(4), wrongFilePath
        )

    def testGetMatContent(self):

        mat, filePath = self._writeMatToFile()

        self.paramReader.setFilePath(filePath)
        matInYamlFile = self.paramReader.getMatContent()

        delta = np.sum(np.abs(matInYamlFile - mat))
        self.assertLess(delta, 1e-10)

    def testGetMatContentWithDefaultSetting(self):

        paramReader = ParamReader()
        matInYamlFile = paramReader.getMatContent()

        self.assertTrue(isinstance(matInYamlFile, np.ndarray))
        self.assertEqual(len(matInYamlFile), 0)

    def testUpdateSettingSeries(self):

        znmaxValue = 20
        zn3IdxValue = [1, 2, 3]
        settingSeries = {"znmax": znmaxValue, "zn3Idx": zn3IdxValue}
        self.paramReader.updateSettingSeries(settingSeries)

        self.assertEqual(self.paramReader.getSetting("znmax"), znmaxValue)
        self.assertEqual(self.paramReader.getSetting("zn3Idx"), zn3IdxValue)

    def testUpdateSetting(self):

        value = 10
        param = "znmax"
        self.paramReader.updateSetting(param, value)

        self.assertEqual(self.paramReader.getSetting(param), value)

    def testUpdateSettingWithWrongParam(self):

        self.assertRaises(ValueError, self.paramReader.updateSetting, "wrongParam", -1)

    def testSaveSettingWithFilePath(self):

        filePath = self._saveSettingFile()

        self.assertTrue(os.path.exists(filePath))
        self.assertEqual(self.paramReader.getFilePath(), filePath)

    def _saveSettingFile(self):

        filePath = os.path.join(self.testTempDir.name, "newConfigFile.yaml")
        self.paramReader.saveSetting(filePath=filePath)

        return filePath

    def testSaveSettingWithoutFilePath(self):

        filePath = self._saveSettingFile()
        paramReader = ParamReader(filePath=filePath)

        paramReader.saveSetting()
        self.assertEqual(paramReader.getFilePath(), filePath)

        # Check the values are saved actually
        self.assertEqual(paramReader.getSetting("znmax"), 22)

        keysInContent = paramReader.getContent().keys()
        self.assertTrue("dofIdx" in keysInContent)
        self.assertTrue("zn3Idx" in keysInContent)

        self.assertEqual(paramReader.getSetting("zn3Idx"), [1] * 19)

    def testGetAbsPathNotExist(self):

        self.assertRaises(
            ValueError, self.paramReader.getAbsPath, "testFile.txt", getModulePath()
        )

    def testGetAbsPath(self):

        filePath = "README.md"
        self.assertFalse(os.path.isabs(filePath))

        filePathAbs = ParamReader.getAbsPath(filePath, getModulePath())
        self.assertTrue(os.path.isabs(filePathAbs))

    def testNonexistentFile(self):

        with self.assertWarns(UserWarning):
            paramReader = ParamReader(filePath="thisFileDoesntExists")
        self.assertEqual(len(paramReader.getContent().keys()), 0)
Ejemplo n.º 6
0
class Instrument(object):
    def __init__(self, instDir):
        """Instrument class for wavefront estimation.

        Parameters
        ----------
        instDir : str
            Instrument configuration directory.
        """

        self.instDir = instDir
        self.instName = ""
        self.dimOfDonutImg = 0
        self.announcedDefocalDisInMm = 0.0

        self.instParamFile = ParamReader()
        self.maskParamFile = ParamReader()

        self.xSensor = np.array([])
        self.ySensor = np.array([])

        self.xoSensor = np.array([])
        self.yoSensor = np.array([])

    def config(
        self,
        camType,
        dimOfDonutImgOnSensor,
        announcedDefocalDisInMm=1.5,
        instParamFileName="instParam.yaml",
        maskMigrateFileName="maskMigrate.yaml",
    ):
        """Do the configuration of Instrument.

        Parameters
        ----------
        camType : enum 'CamType'
            Camera type.
        dimOfDonutImgOnSensor : int
            Dimension of donut image on sensor in pixel.
        announcedDefocalDisInMm : float
            Announced defocal distance in mm. It is noted that the defocal
            distance offset used in calculation might be different from this
            value. (the default is 1.5.)
        instParamFileName : str, optional
            Instrument parameter file name. (the default is "instParam.yaml".)
        maskMigrateFileName : str, optional
            Mask migration (off-axis correction) file name. (the default is
            "maskMigrate.yaml".)
        """

        self.instName = self._getInstName(camType)
        self.dimOfDonutImg = int(dimOfDonutImgOnSensor)
        self.announcedDefocalDisInMm = announcedDefocalDisInMm

        # Path of instrument param file
        instFileDir = self.getInstFileDir()
        instParamFilePath = os.path.join(instFileDir, instParamFileName)
        self.instParamFile.setFilePath(instParamFilePath)

        # Path of mask off-axis correction file
        # There is no such file for auxiliary telescope
        maskParamFilePath = os.path.join(instFileDir, maskMigrateFileName)
        if os.path.exists(maskParamFilePath):
            self.maskParamFile.setFilePath(maskParamFilePath)

        self._setSensorCoor()
        self._setSensorCoorAnnular()

    def _getInstName(self, camType):
        """Get the instrument name.

        Parameters
        ----------
        camType : enum 'CamType'
            Camera type.

        Returns
        -------
        str
            Instrument name.

        Raises
        ------
        ValueError
            Camera type is not supported.
        """

        if camType == CamType.LsstCam:
            return "lsst"
        elif camType == CamType.LsstFamCam:
            return "lsstfam"
        elif camType == CamType.ComCam:
            return "comcam"
        elif camType == CamType.AuxTel:
            return "auxTel"
        else:
            raise ValueError("Camera type (%s) is not supported." % camType)

    def getInstFileDir(self):
        """Get the instrument parameter file directory.

        Returns
        -------
        str
            Instrument parameter file directory.
        """

        return os.path.join(self.instDir, self.instName)

    def _setSensorCoor(self):
        """Set the sensor coordinate."""

        # 0.5 is the half of single pixel
        ySensorGrid, xSensorGrid = np.mgrid[-(self.dimOfDonutImg / 2 - 0.5):(
            self.dimOfDonutImg / 2 +
            0.5), -(self.dimOfDonutImg / 2 - 0.5):(self.dimOfDonutImg / 2 +
                                                   0.5), ]

        sensorFactor = self.getSensorFactor()
        denominator = self.dimOfDonutImg / 2 / sensorFactor

        self.xSensor = xSensorGrid / denominator
        self.ySensor = ySensorGrid / denominator

    def _setSensorCoorAnnular(self):
        """Set the sensor coordinate with the annular aperature."""

        self.xoSensor = self.xSensor.copy()
        self.yoSensor = self.ySensor.copy()

        # Get the position index that is out of annular aperature range
        obscuration = self.getObscuration()
        r2Sensor = self.xSensor**2 + self.ySensor**2
        idx = (r2Sensor > 1) | (r2Sensor < obscuration**2)

        # Define the value to be NaN if it is not in pupul
        self.xoSensor[idx] = np.nan
        self.yoSensor[idx] = np.nan

    def setAnnDefocalDisInMm(self, annDefocalDisInMm):
        """Set the announced defocal distance in mm.

        Parameters
        ----------
        annDefocalDisInMm : float
            Announced defocal distance in mm.
        """

        self.announcedDefocalDisInMm = annDefocalDisInMm

    def getAnnDefocalDisInMm(self):
        """Get the announced defocal distance in mm.

        Returns
        -------
        float
            Announced defocal distance in mm.
        """

        return self.announcedDefocalDisInMm

    def getInstFilePath(self):
        """Get the instrument parameter file path.

        Returns
        -------
        str
            Instrument parameter file path.
        """

        return self.instParamFile.getFilePath()

    def getMaskOffAxisCorr(self):
        """Get the mask off-axis correction.

        Returns
        -------
        numpy.ndarray
            Mask off-axis correction.
        """

        return self.maskParamFile.getMatContent()

    def getDimOfDonutOnSensor(self):
        """Get the dimension of donut image size on sensor in pixel.

        Returns
        -------
        int
            Dimension of donut's size on sensor in pixel.
        """

        return self.dimOfDonutImg

    def getObscuration(self):
        """Get the obscuration.

        Returns
        -------
        float
            Obscuration.
        """

        return self.instParamFile.getSetting("obscuration")

    def getFocalLength(self):
        """Get the focal length of telescope in meter.

        Returns
        -------
        float
            Focal length of telescope in meter.
        """

        return self.instParamFile.getSetting("focalLength")

    def getApertureDiameter(self):
        """Get the aperture diameter in meter.

        Returns
        -------
        float
            Aperture diameter in meter.
        """

        return self.instParamFile.getSetting("apertureDiameter")

    def getDefocalDisOffset(self):
        """Get the defocal distance offset in meter.

        Returns
        -------
        float
            Defocal distance offset in meter.
        """

        # Use this way to differentiate the announced defocal distance
        # and the used defocal distance in the fitting of parameters by ZEMAX
        # For example, in the ComCam's instParam.yaml file, they are a little
        # different
        offset = self.instParamFile.getSetting("offset")
        defocalDisInMm = "%.1fmm" % self.announcedDefocalDisInMm

        return offset[defocalDisInMm]

    def getCamPixelSize(self):
        """Get the camera pixel size in meter.

        Returns
        -------
        float
            Camera pixel size in meter.
        """

        return self.instParamFile.getSetting("pixelSize")

    def getMarginalFocalLength(self):
        """Get the marginal focal length in meter.

        Marginal_focal_length = sqrt(f^2 - (D/2)^2)

        Returns
        -------
        float
            Marginal focal length in meter.
        """

        focalLength = self.getFocalLength()
        apertureDiameter = self.getApertureDiameter()
        marginalFL = np.sqrt(focalLength**2 - (apertureDiameter / 2)**2)

        return marginalFL

    def getSensorFactor(self):
        """Get the sensor factor.

        Returns
        -------
        float
            Sensor factor.
        """

        offset = self.getDefocalDisOffset()
        apertureDiameter = self.getApertureDiameter()
        focalLength = self.getFocalLength()
        pixelSize = self.getCamPixelSize()
        sensorFactor = self.dimOfDonutImg / (offset * apertureDiameter /
                                             focalLength / pixelSize)

        return sensorFactor

    def getSensorCoor(self):
        """Get the sensor coordinate.

        Returns
        -------
        numpy.ndarray
            X coordinate.
        numpy.ndarray
            Y coordinate.
        """

        return self.xSensor, self.ySensor

    def getSensorCoorAnnular(self):
        """Get the sensor coordinate with the annular aperature.

        Returns
        -------
        numpy.ndarray
            X coordinate.
        numpy.ndarray
            Y coordinate.
        """

        return self.xoSensor, self.yoSensor

    def calcSizeOfDonutExpected(self):
        """Calculate the size of expected donut (diameter).

        Returns
        -------
        float
            Size of expected donut (diameter) in pixel.
        """

        offset = self.getDefocalDisOffset()
        fNumber = self.getFocalLength() / self.getApertureDiameter()
        pixelSize = self.getCamPixelSize()

        return offset / fNumber / pixelSize
Ejemplo n.º 7
0
class Algorithm(object):
    def __init__(self, algoDir):
        """Initialize the Algorithm class.

        Algorithm used to solve the transport of intensity equation to get
        normal/ annular Zernike polynomials.

        Parameters
        ----------
        algoDir : str
            Algorithm configuration directory.
        """

        self.algoDir = algoDir
        self.algoParamFile = ParamReader()

        self._inst = Instrument("")

        # Show the calculation message based on this value
        # 0 means no message will be showed
        self.debugLevel = 0

        # Image has the problem or not from the over-compensation
        self.caustic = False

        # Record the Zk coefficients in each outer-loop iteration
        # The actual total outer-loop iteration time is Num_of_outer_itr + 1
        self.converge = np.array([])

        # Current number of outer-loop iteration
        self.currentItr = 0

        # Record the coefficients of normal/ annular Zernike polynomials after
        # z4 in unit of nm
        self.zer4UpNm = np.array([])

        # Converged wavefront.
        self.wcomp = np.array([])

        # Calculated wavefront in previous outer-loop iteration.
        self.West = np.array([])

        # Converged Zk coefficients
        self.zcomp = np.array([])

        # Calculated Zk coefficients in previous outer-loop iteration
        self.zc = np.array([])

        # Padded mask for use at the offset planes
        self.pMask = None

        # Non-padded mask corresponding to aperture
        self.cMask = None

        # Change the dimension of mask for fft to use
        self.pMaskPad = None
        self.cMaskPad = None

    def reset(self):
        """Reset the calculation for the new input images with the same
        algorithm settings."""

        self.caustic = False
        self.converge = np.zeros(self.converge.shape)
        self.currentItr = 0
        self.zer4UpNm = np.zeros(self.zer4UpNm.shape)

        self.wcomp = np.zeros(self.wcomp.shape)
        self.West = np.zeros(self.West.shape)

        self.zcomp = np.zeros(self.zcomp.shape)
        self.zc = np.zeros(self.zc.shape)

        self.pMask = None
        self.cMask = None

        self.pMaskPad = None
        self.cMaskPad = None

    def config(self, algoName, inst, debugLevel=0):
        """Configure the algorithm to solve TIE.

        Parameters
        ----------
        algoName : str
            Algorithm configuration file to solve the Poisson's equation in the
            transport of intensity equation (TIE). It can be "fft" or "exp"
            here.
        inst : Instrument
            Instrument to use.
        debugLevel : int, optional
            Show the information under the running. If the value is higher, the
            information shows more. It can be 0, 1, 2, or 3. (the default is
            0.)
        """

        algoParamFilePath = os.path.join(self.algoDir, "%s.yaml" % algoName)
        self.algoParamFile.setFilePath(algoParamFilePath)

        self._inst = inst
        self.debugLevel = debugLevel

        self.caustic = False

        numTerms = self.getNumOfZernikes()
        outerItr = self.getNumOfOuterItr()
        self.converge = np.zeros((numTerms, outerItr + 1))

        self.currentItr = 0

        self.zer4UpNm = np.zeros(numTerms - 3)

        # Wavefront related parameters
        dimOfDonut = self._inst.getDimOfDonutOnSensor()
        self.wcomp = np.zeros((dimOfDonut, dimOfDonut))
        self.West = self.wcomp.copy()

        # Used in model basis ("zer").
        self.zcomp = np.zeros(numTerms)
        self.zc = self.zcomp.copy()

        # Mask related variables
        self.pMask = None
        self.cMask = None
        self.pMaskPad = None
        self.cMaskPad = None

    def setDebugLevel(self, debugLevel):
        """Set the debug level.

        If the value is higher, the information shows more. It can be 0, 1, 2,
        or 3.

        Parameters
        ----------
        debugLevel : int
            Show the information under the running.
        """

        self.debugLevel = int(debugLevel)

    def getDebugLevel(self):
        """Get the debug level.

        If the value is higher, the information shows more. It can be 0, 1, 2,
        or 3.

        Returns
        -------
        int
            Debug level.
        """

        return self.debugLevel

    def getZer4UpInNm(self):
        """Get the coefficients of Zernike polynomials of z4-zn in nm.

        Returns
        -------
        numpy.ndarray
            Zernike polynomials of z4-zn in nm.
        """

        return self.zer4UpNm

    def getPoissonSolverName(self):
        """Get the method name to solve the Poisson equation.

        Returns
        -------
        str
            Method name to solve the Poisson equation.
        """

        return self.algoParamFile.getSetting("poissonSolver")

    def getNumOfZernikes(self):
        """Get the maximum number of Zernike polynomials supported.

        Returns
        -------
        int
            Maximum number of Zernike polynomials supported.
        """

        return int(self.algoParamFile.getSetting("numOfZernikes"))

    def getZernikeTerms(self):
        """Get the Zernike terms in using.

        Returns
        -------
        list[int]
            Zernike terms in using.
        """

        numTerms = self.getNumOfZernikes()

        return list(range(numTerms))

    def getObsOfZernikes(self):
        """Get the obscuration of annular Zernike polynomials.

        Returns
        -------
        float
            Obscuration of annular Zernike polynomials
        """

        zobsR = self.algoParamFile.getSetting("obsOfZernikes")
        if zobsR == 1:
            zobsR = self._inst.getObscuration()

        return float(zobsR)

    def getNumOfOuterItr(self):
        """Get the number of outer loop iteration.

        Returns
        -------
        int
            Number of outer loop iteration.
        """

        return int(self.algoParamFile.getSetting("numOfOuterItr"))

    def getNumOfInnerItr(self):
        """Get the number of inner loop iteration.

        This is for the fast Fourier transform (FFT) solver only.

        Returns
        -------
        int
            Number of inner loop iteration.
        """

        return int(self.algoParamFile.getSetting("numOfInnerItr"))

    def getFeedbackGain(self):
        """Get the gain value used in the outer loop iteration.

        Returns
        -------
        float
            Gain value used in the outer loop iteration.
        """

        return self.algoParamFile.getSetting("feedbackGain")

    def getOffAxisPolyOrder(self):
        """Get the number of polynomial order supported in off-axis correction.

        Returns
        -------
        int
            Number of polynomial order supported in off-axis correction.
        """

        return int(self.algoParamFile.getSetting("offAxisPolyOrder"))

    def getCompensatorMode(self):
        """Get the method name to compensate the wavefront by wavefront error.

        Returns
        -------
        str
            Method name to compensate the wavefront by wavefront error.
        """

        return self.algoParamFile.getSetting("compensatorMode")

    def getCompSequence(self):
        """Get the compensated sequence of Zernike order for each iteration.

        Returns
        -------
        numpy.ndarray[int]
            Compensated sequence of Zernike order for each iteration.
        """

        compSequenceFromFile = self.algoParamFile.getSetting("compSequence")
        compSequence = np.array(compSequenceFromFile, dtype=int)

        # If outerItr is large, and compSequence is too small,
        # the rest in compSequence will be filled.
        # This is used in the "zer" method.
        outerItr = self.getNumOfOuterItr()
        compSequence = self._extend1dArray(compSequence, outerItr)
        compSequence = compSequence.astype(int)

        return compSequence

    def _extend1dArray(self, origArray, targetLength):
        """Extend the 1D original array to the taget length.

        The extended value will be the final element of original array. Nothing
        will be done if the input array is not 1D or its length is less than
        the target.

        Parameters
        ----------
        origArray : numpy.ndarray
            Original array with 1 dimension.
        targetLength : int
            Target length of new extended array.

        Returns
        -------
        numpy.ndarray
            Extended 1D array.
        """

        if (len(origArray) < targetLength) and (origArray.ndim == 1):
            leftOver = np.ones(targetLength - len(origArray))
            extendArray = np.append(origArray, origArray[-1] * leftOver)
        else:
            extendArray = origArray

        return extendArray

    def getBoundaryThickness(self):
        """Get the boundary thickness that the computation mask extends beyond
        the pupil mask.

        It is noted that in Fast Fourier transform (FFT) algorithm, it is also
        the width of Neuman boundary where the derivative of the wavefront is
        set to zero

        Returns
        -------
        int
            Boundary thickness.
        """

        return int(self.algoParamFile.getSetting("boundaryThickness"))

    def getFftDimension(self):
        """Get the FFT pad dimension in pixel.

        This is for the fast Fourier transform (FFT) solver only.

        Returns
        -------
        int
            FFT pad dimention.
        """

        fftDim = int(self.algoParamFile.getSetting("fftDimension"))

        # Make sure the dimension is the order of multiple of 2
        if fftDim == 999:
            dimToFit = self._inst.getDimOfDonutOnSensor()
        else:
            dimToFit = fftDim

        padDim = int(2 ** np.ceil(np.log2(dimToFit)))

        return padDim

    def getSignalClipSequence(self):
        """Get the signal clip sequence.

        The number of values should be the number of compensation plus 1.
        This is for the fast Fourier transform (FFT) solver only.

        Returns
        -------
        numpy.ndarray
            Signal clip sequence.
        """

        sumclipSequenceFromFile = self.algoParamFile.getSetting("signalClipSequence")
        sumclipSequence = np.array(sumclipSequenceFromFile)

        # If outerItr is large, and sumclipSequence is too small, the rest in
        # sumclipSequence will be filled.
        # This is used in the "zer" method.
        targetLength = self.getNumOfOuterItr() + 1
        sumclipSequence = self._extend1dArray(sumclipSequence, targetLength)

        return sumclipSequence

    def getMaskScalingFactor(self):
        """Get the mask scaling factor for fast beam.

        Returns
        -------
        float
            Mask scaling factor for fast beam.
        """

        # m = R'*f/(l*R), R': radius of the no-aberration image
        focalLength = self._inst.getFocalLength()
        marginalFL = self._inst.getMarginalFocalLength()
        maskScalingFactor = focalLength / marginalFL

        return maskScalingFactor

    def getWavefrontMapEsti(self):
        """Get the estimated wavefront map.

        Returns
        -------
        numpy.ndarray
            Estimated wavefront map.
        """

        return self._getWavefrontMapWithMaskApplied(self.wcomp)

    def getWavefrontMapResidual(self):
        """Get the residual wavefront map.

        Returns
        -------
        numpy.ndarray
            Residual wavefront map.
        """

        return self._getWavefrontMapWithMaskApplied(self.West)

    def _getWavefrontMapWithMaskApplied(self, wfMap):
        """Get the wavefront map with mask applied.

        Parameters
        ----------
        wfMap : numpy.ndarray
            Wavefront map.

        Returns
        -------
        numpy.ndarray
            Wavefront map with mask applied.
        """

        self._checkNotItr0()

        wfMapWithMask = wfMap.copy()
        wfMapWithMask[self.pMask == 0] = np.nan

        return wfMapWithMask

    def _checkNotItr0(self):
        """Check not in the iteration 0.

        TIE: Transport of intensity equation.

        Raises
        ------
        RuntimeError
            Need to solve the TIE first.
        """

        if self.currentItr == 0:
            raise RuntimeError("Need to solve the TIE first.")

    def itr0(self, I1, I2, model):
        """Calculate the wavefront and coefficients of normal/ annular Zernike
        polynomials in the first iteration time.

        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".
        """

        # Reset the iteration time of outer loop and decide to reset the
        # defocal images or not
        self._reset(I1, I2)

        # Solve the transport of intensity equation (TIE)
        self._singleItr(I1, I2, model)

    def runIt(self, I1, I2, model, tol=1e-3):
        """Calculate the wavefront error by solving the transport of intensity
        equation (TIE).

        The inner (for fft algorithm) and outer loops are used. The inner loop
        is to solve the Poisson's equation. The outer loop is to compensate the
        intra- and extra-focal images to mitigate the calculation of wavefront
        (e.g. 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.)
        """

        # To have the iteration time initiated from global variable is to
        # distinguish the manually and automatically iteration processes.
        itr = self.currentItr
        while itr <= self.getNumOfOuterItr():
            stopItr = self._singleItr(I1, I2, model, tol)

            # Stop the iteration of outer loop if converged
            if stopItr:
                break

            itr += 1

    def nextItr(self, I1, I2, model, nItr=1):
        """Run the outer loop iteration with the specific time defined in nItr.

        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".
        nItr : int, optional
            Outer loop iteration time. (the default is 1.)
        """

        #  Do the iteration
        ii = 0
        while ii < nItr:
            self._singleItr(I1, I2, model)
            ii += 1

    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

    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 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.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.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

            # 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.0) ** 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()

            # 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 _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

    def _getdIandI(self, I1, I2):
        """Calculate the central image and differential image to be used in the
        serial expansion method.

        It is noted that the images are assumed to be co-center already. And
        the intra-/ extra-focal image can overlap with one another after the
        rotation of 180 degree.

        Parameters
        ----------
        I1 : CompensableImage
            Intra- or extra-focal image.
        I2 : CompensableImage
            Intra- or extra-focal image.

        Returns
        -------
        numpy.ndarray
            Image data of I0.
        numpy.ndarray
            Differential image (dI) of I0.
        """

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

        # Calculate the central image and differential iamge
        I0 = (I1image + I2image) / 2
        dI = I2image - I1image

        return I0, dI

    def _checkImageDim(self, I1, I2):
        """Check the dimension of images.

        It is noted that the I2 image is rotated by 180 degree.

        Parameters
        ----------
        I1 : CompensableImage
            Intra- or extra-focal image.
        I2 : CompensableImage
            Intra- or extra-focal image.

        Returns
        -------
        numpy.ndarray
            I1 defocal image.
        numpy.ndarray
            I2 defocal image. It is noted that the I2 image is rotated by 180
            degree.

        Raises
        ------
        Exception
            Check the dimension of images is n by n or not.
        Exception
            Check two defocal images have the same size or not.
        """

        # Check the condition of images
        m1, n1 = I1.getImg().shape
        m2, n2 = I2.getImg().shape

        if m1 != n1 or m2 != n2:
            raise Exception("Image is not square.")

        if m1 != m2 or n1 != n2:
            raise Exception("Images do not have the same size.")

        # Define I1
        I1image = I1.getImg()

        # Rotate the image by 180 degree through rotating two times of 90
        # degree
        I2image = np.rot90(I2.getImg(), k=2)

        return I1image, I2image

    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)

    def _applyI1I2pMask(self, I1, I2):
        """Correct the defocal images if I1 and I2 are belong to different
        sources.

        (There is a problem for this actually. If I1 and I2 come from different
        sources, what should the correction of TIE be? At this moment, the
        fieldX and fieldY of I1 and I2 should be different. And the sources are
        different also.)

        Parameters
        ----------
        I1 : CompensableImage
            Intra- or extra-focal image.
        I2 : CompensableImage
            Intra- or extra-focal image.

        Returns
        -------
        numpy.ndarray
            Corrected I1 image.
        numpy.ndarray
            Corrected I2 image.
        """

        # Get the overlap region of images and do the normalization.
        if I1.getFieldXY() != I2.getFieldXY():

            # Get the overlap region of image
            I1.updateImage(I1.getImg() * self.pMask)

            # Rotate the pMask by 180 degree through rotating two times of 90
            # degree because I2 has been rotated by 180 degree already.
            I2.updateImage(I2.getImg() * np.rot90(self.pMask, 2))

            # Do the normalization of image.
            I1.updateImage(I1.getImg() / np.sum(I1.getImg()))
            I2.updateImage(I2.getImg() / np.sum(I2.getImg()))

        # Return the correct images. It is noted that there is no need of
        # vignetting correction.
        # This is after masking already in _singleItr() or itr0().
        return I1, I2

    def _reset(self, I1, I2):
        """Reset the iteration time of outer loop and defocal images.

        Parameters
        ----------
        I1 : CompensableImage
            Intra- or extra-focal image.
        I2 : CompensableImage
            Intra- or extra-focal image.
        """

        # Reset the current iteration time to 0
        self.currentItr = 0

        # Show the reset information
        if self.debugLevel >= 3:
            print("Resetting images: I1 and I2")

        # Determine to reset the images or not based on the existence of
        # the attribute: Image.image0. Only after the first run of
        # inner loop, this attribute will exist.
        try:
            # Reset the images to the first beginning
            I1.updateImage(I1.getImgInit().copy())
            I2.updateImage(I2.getImgInit().copy())

            # Show the information of resetting image
            if self.debugLevel >= 3:
                print("Resetting images in inside.")

        except AttributeError:
            # Show the information of no image0
            if self.debugLevel >= 3:
                print("Image0 = None. This is the first time to run the code.")

            pass

    def outZer4Up(self, unit="nm", filename=None, showPlot=False):
        """Put the coefficients of normal/ annular Zernike polynomials on
        terminal or file ande show the image if it is needed.

        Parameters
        ----------
        unit : str, optional
            Unit of the coefficients of normal/ annular Zernike polynomials. It
            can be m, nm, or um. (the default is "nm".)
        filename : str, optional
            Name of output file. (the default is None.)
        showPlot : bool, optional
            Decide to show the plot or not. (the default is False.)
        """

        # List of Zn,m
        Znm = [
            "Z0,0",
            "Z1,1",
            "Z1,-1",
            "Z2,0",
            "Z2,-2",
            "Z2,2",
            "Z3,-1",
            "Z3,1",
            "Z3,-3",
            "Z3,3",
            "Z4,0",
            "Z4,2",
            "Z4,-2",
            "Z4,4",
            "Z4,-4",
            "Z5,1",
            "Z5,-1",
            "Z5,3",
            "Z5,-3",
            "Z5,5",
            "Z5,-5",
            "Z6,0",
        ]

        # Decide the format of z based on the input unit (m, nm, or um)
        if unit == "m":
            z = self.zer4UpNm * 1e-9
        elif unit == "nm":
            z = self.zer4UpNm
        elif unit == "um":
            z = self.zer4UpNm * 1e-3
        else:
            print("Unknown unit: %s" % unit)
            print("Unit options are: m, nm, um")
            return

        # Write the coefficients into a file if needed.
        if filename is not None:
            f = open(filename, "w")
        else:
            f = sys.stdout

        for ii in range(4, len(z) + 4):
            f.write("Z%d (%s)\t %8.3f\n" % (ii, Znm[ii - 1], z[ii - 4]))

        # Close the file
        if filename is not None:
            f.close()

        # Show the plot
        if showPlot:
            zkIdx = range(4, len(z) + 4)
            plotZernike(zkIdx, z, unit)
Ejemplo n.º 8
0
class TestParamReader(unittest.TestCase):
    """Test the ParamReaderYaml class."""

    def setUp(self):

        testDir = os.path.join(getModulePath(), "tests")
        self.configDir = os.path.join(testDir, "testData")
        self.fileName = "testConfigFile.yaml"

        filePath = os.path.join(self.configDir, self.fileName)
        self.paramReader = ParamReader(filePath=filePath)

        self.testTempDir = os.path.join(testDir, "tmp")
        self._makeDir(self.testTempDir)

    def _makeDir(self, directory):

        if (not os.path.exists(directory)):
            os.makedirs(directory)

    def tearDown(self):

        shutil.rmtree(self.testTempDir)

    def testGetSetting(self):

        znmax = self.paramReader.getSetting("znmax")
        self.assertEqual(znmax, 22)

    def testGetFilePath(self):

        ansFilePath = os.path.join(self.configDir, self.fileName)
        self.assertEqual(self.paramReader.getFilePath(), ansFilePath)

    def testSetFilePath(self):

        fileName = "test.yaml"
        filePath = os.path.join(self.configDir, fileName)
        self.paramReader.setFilePath(filePath)

        self.assertEqual(self.paramReader.getFilePath(), filePath)

    def testGetContent(self):

        content = self.paramReader.getContent()
        self.assertTrue(isinstance(content, dict))

    def testGetContentWithDefaultSetting(self):

        paramReader = ParamReader()

        content = paramReader.getContent()
        self.assertTrue(isinstance(content, dict))

    def testWriteMatToFile(self):

        self._writeMatToFile()

        numOfFile = self._getNumOfFileInFolder(self.testTempDir)
        self.assertEqual(numOfFile, 1)

    def _writeMatToFile(self):

        mat = np.random.rand(3, 4, 5)
        filePath = os.path.join(self.testTempDir, "temp.yaml")
        ParamReader.writeMatToFile(mat, filePath)

        return mat, filePath

    def _getNumOfFileInFolder(self, folder):

        return len([name for name in os.listdir(folder)
                   if os.path.isfile(os.path.join(folder, name))])

    def testWriteMatToFileWithWrongFileFormat(self):

        wrongFilePath = os.path.join(self.testTempDir, "temp.txt")
        self.assertRaises(ValueError, ParamReader.writeMatToFile,
                          np.ones(4), wrongFilePath)

    def testGetMatContent(self):

        mat, filePath = self._writeMatToFile()

        self.paramReader.setFilePath(filePath)
        matInYamlFile = self.paramReader.getMatContent()

        delta = np.sum(np.abs(matInYamlFile - mat))
        self.assertLess(delta, 1e-10)

    def testGetMatContentWithDefaultSetting(self):

        paramReader = ParamReader()
        matInYamlFile = paramReader.getMatContent()

        self.assertTrue(isinstance(matInYamlFile, np.ndarray))
        self.assertEqual(len(matInYamlFile), 0)
Ejemplo n.º 9
0
class Instrument(object):

    def __init__(self, instDir):
        """Instrument class for wavefront estimation.

        Parameters
        ----------
        instDir : str
            Instrument configuration directory.
        """

        self.instDir = instDir
        self.instName = ""
        self.dimOfDonut = 0
        self.announcedDefocalDisInMm = 0.0

        self.instParamFile = ParamReader()
        self.maskParamFile = ParamReader()

        self.xSensor = np.array([])
        self.ySensor = np.array([])

        self.xoSensor = np.array([])
        self.yoSensor = np.array([])

    def config(self, camType, dimOfDonutOnSensor,
               announcedDefocalDisInMm=1.5,
               instParamFileName="instParam.yaml",
               maskMigrateFileName="maskMigrate.yaml"):
        """Do the configuration of Instrument.

        Parameters
        ----------
        camType : enum 'CamType'
            Camera type.
        dimOfDonutOnSensor : int
            Dimension of image on sensor in pixel.
        announcedDefocalDisInMm : float
            Announced defocal distance in mm. It is noted that the defocal
            distance offset used in calculation might be different from this
            value. (the default is 1.5.)
        instParamFileName : str, optional
            Instrument parameter file name. (the default is "instParam.yaml".)
        maskMigrateFileName : str, optional
            Mask migration (off-axis correction) file name. (the default is
            "maskMigrate.yaml".)
        """

        self.instName = self._getInstName(camType)
        self.dimOfDonut = int(dimOfDonutOnSensor)
        self.announcedDefocalDisInMm = announcedDefocalDisInMm

        # Path of instrument param file
        instFileDir = self.getInstFileDir()
        instParamFilePath = os.path.join(instFileDir, instParamFileName)
        self.instParamFile.setFilePath(instParamFilePath)

        # Path of mask off-axis correction file
        maskParamFilePath = os.path.join(instFileDir, maskMigrateFileName)
        self.maskParamFile.setFilePath(maskParamFilePath)

        self._setSensorCoor()
        self._setSensorCoorAnnular()

    def _getInstName(self, camType):
        """Get the instrument name.

        Parameters
        ----------
        camType : enum 'CamType'
            Camera type.

        Returns
        -------
        str
            Instrument name.

        Raises
        ------
        ValueError
            Camera type is not supported.
        """

        if (camType == CamType.LsstCam):
            return "lsst"
        elif (camType == CamType.ComCam):
            return "comcam"
        else:
            raise ValueError("Camera type (%s) is not supported." % camType)

    def getInstFileDir(self):
        """Get the instrument parameter file directory.

        Returns
        -------
        str
            Instrument parameter file directory.
        """

        return os.path.join(self.instDir, self.instName)

    def _setSensorCoor(self):
        """Set the sensor coordinate."""

        ySensorGrid, xSensorGrid = np.mgrid[
            -(self.dimOfDonut/2-0.5):(self.dimOfDonut/2 + 0.5),
            -(self.dimOfDonut/2-0.5):(self.dimOfDonut/2 + 0.5)]

        sensorFactor = self.getSensorFactor()
        denominator = self.dimOfDonut / 2 / sensorFactor

        self.xSensor = xSensorGrid / denominator
        self.ySensor = ySensorGrid / denominator

    def _setSensorCoorAnnular(self):
        """Set the sensor coordinate with the annular aperature."""

        self.xoSensor = self.xSensor.copy()
        self.yoSensor = self.ySensor.copy()

        # Get the position index that is out of annular aperature range
        obscuration = self.getObscuration()
        r2Sensor = self.xSensor**2 + self.ySensor**2
        idx = (r2Sensor > 1) | (r2Sensor < obscuration**2)

        # Define the value to be NaN if it is not in pupul
        self.xoSensor[idx] = np.nan
        self.yoSensor[idx] = np.nan

    def setAnnDefocalDisInMm(self, annDefocalDisInMm):
        """Set the announced defocal distance in mm.

        Parameters
        ----------
        annDefocalDisInMm : float
            Announced defocal distance in mm.
        """

        self.announcedDefocalDisInMm = annDefocalDisInMm

    def getAnnDefocalDisInMm(self):
        """Get the announced defocal distance in mm.

        Returns
        -------
        float
            Announced defocal distance in mm.
        """

        return self.announcedDefocalDisInMm

    def getInstFilePath(self):
        """Get the instrument parameter file path.

        Returns
        -------
        str
            Instrument parameter file path.
        """

        return self.instParamFile.getFilePath()

    def getMaskOffAxisCorr(self):
        """Get the mask off-axis correction.

        Returns
        -------
        numpy.ndarray
            Mask off-axis correction.
        """

        return self.maskParamFile.getMatContent()

    def getDimOfDonutOnSensor(self):
        """Get the dimension of donut's size on sensor in pixel.

        Returns
        -------
        int
            Dimension of donut's size on sensor in pixel.
        """

        return self.dimOfDonut

    def getObscuration(self):
        """Get the obscuration.

        Returns
        -------
        float
            Obscuration.
        """

        return self.instParamFile.getSetting("obscuration")

    def getFocalLength(self):
        """Get the focal length of telescope in meter.

        Returns
        -------
        float
            Focal length of telescope in meter.
        """

        return self.instParamFile.getSetting("focalLength")

    def getApertureDiameter(self):
        """Get the aperture diameter in meter.

        Returns
        -------
        float
            Aperture diameter in meter.
        """

        return self.instParamFile.getSetting("apertureDiameter")

    def getDefocalDisOffset(self):
        """Get the defocal distance offset in meter.

        Returns
        -------
        float
            Defocal distance offset in meter.
        """

        offset = self.instParamFile.getSetting("offset")
        defocalDisInMm = "%.1fmm" % self.announcedDefocalDisInMm

        return offset[defocalDisInMm]

    def getCamPixelSize(self):
        """Get the camera pixel size in meter.

        Returns
        -------
        float
            Camera pixel size in meter.
        """

        return self.instParamFile.getSetting("pixelSize")

    def getMarginalFocalLength(self):
        """Get the marginal focal length in meter.

        Marginal_focal_length = sqrt(f^2 - (D/2)^2)

        Returns
        -------
        float
            Marginal focal length in meter.
        """

        focalLength = self.getFocalLength()
        apertureDiameter = self.getApertureDiameter()
        marginalFL = np.sqrt(focalLength**2 - (apertureDiameter/2)**2)

        return marginalFL

    def getSensorFactor(self):
        """Get the sensor factor.

        Returns
        -------
        float
            Sensor factor.
        """

        offset = self.getDefocalDisOffset()
        apertureDiameter = self.getApertureDiameter()
        focalLength = self.getFocalLength()
        pixelSize = self.getCamPixelSize()
        sensorFactor = self.dimOfDonut / (
            offset * apertureDiameter / focalLength / pixelSize)

        return sensorFactor

    def getSensorCoor(self):
        """Get the sensor coordinate.

        Returns
        -------
        numpy.ndarray
            X coordinate.
        numpy.ndarray
            Y coordinate.
        """

        return self.xSensor, self.ySensor

    def getSensorCoorAnnular(self):
        """Get the sensor coordinate with the annular aperature.

        Returns
        -------
        numpy.ndarray
            X coordinate.
        numpy.ndarray
            Y coordinate.
        """

        return self.xoSensor, self.yoSensor
Ejemplo n.º 10
0
class MirrorSim(object):
    def __init__(self, innerRinM, outerRinM, mirrorDataDir):
        """Initiate the mirror simulator class.

        Parameters
        ----------
        innerRinM : float or tuple
            Mirror inner radius in m.
        outerRinM : float or tuple
            Mirror outer radius in m.
        mirrorDataDir : str
            Mirror data directory.
        """

        # Mirror inner radius
        self.radiusInner = innerRinM

        # Mirror outer radius
        self.radiusOuter = outerRinM

        # Configuration data directory
        self.mirrorDataDir = mirrorDataDir

        # Mirror actuator force
        self._actForceFile = ParamReader()

        # Look-up table (LUT) file
        self._lutFile = ParamReader()

        # Mirror surface
        self._surf = np.array([])

        # Number of Zernike terms to fit.
        self._numTerms = 0

    def config(self, numTerms=28, actForceFileName="", lutFileName=""):
        """Do the configuration.

        LUT: Look-up table.

        Parameters
        ----------
        numTerms : int, optional
            Number of Zernike terms to fit. (the default is 28.)
        actForceFileName : str
            Actuator force file name. (the default is "".)
        lutFileName : str, optional
            LUT file name. (the default is "".)
        """

        self._numTerms = int(numTerms)

        if (actForceFileName != ""):
            actForceFilePath = os.path.join(self.mirrorDataDir,
                                            actForceFileName)
            self._actForceFile.setFilePath(actForceFilePath)

        if (lutFileName != ""):
            lutFilePath = os.path.join(self.mirrorDataDir, lutFileName)
            self._lutFile.setFilePath(lutFilePath)

    def getNumTerms(self):
        """Get the number of Zernike terms to fit.

        Returns
        -------
        int
            Number of Zernike terms to fit.
        """

        return self._numTerms

    def getInnerRinM(self):
        """Get the inner radius of mirror in meter.

        Returns
        -------
        float or tuple
            Inner radius of mirror in meter.
        """

        return self.radiusInner

    def getOuterRinM(self):
        """Get the outer radius of mirror in meter.

        Returns
        -------
        float or tuple
            Outer radius of mirror in meter.
        """

        return self.radiusOuter

    def getMirrorDataDir(self):
        """Get the directory of mirror data.

        Returns
        -------
        str
            Directory to mirror data.
        """

        return self.mirrorDataDir

    def setSurfAlongZ(self, surfAlongZinUm):
        """Set the mirror surface along the z direction in um.

        Parameters
        ----------
        surfAlongZinUm : numpy.ndarray
            Mirror surface along the z direction in um.
        """

        self._surf = np.array(surfAlongZinUm, dtype=float)

    def getSurfAlongZ(self):
        """Get the mirror surface along the z direction in um.

        Returns
        -------
        numpy.ndarray
            Mirror surface along the z direction in um.
        """

        return self._surf

    def getLUTforce(self, zangleInDeg):
        """Get the actuator force of mirror based on LUT.

        LUT: Look-up table.

        Parameters
        ----------
        zangleInDeg : float
            Zenith angle in degree.

        Returns
        -------
        numpy.ndarray
            Actuator forces in specific zenith angle.

        Raises
        ------
        ValueError
            The degee order in LUT is incorrect.
        """

        # Read the LUT file
        lut = self._lutFile.getMatContent()

        # Get the step. The values of LUT are listed in every step size.
        # The degree range is 0 - 90 degree.
        # The file in the simulation is every 1 degree. The formal one should
        # be every 5 degree.
        ruler = lut[0, :]
        stepList = np.diff(ruler)
        if np.any(stepList <= 0):
            raise ValueError("The degee order in LUT is incorrect.")

        # If the specific zenith angle is larger than the listed angle range,
        # use the biggest listed zenith angle data instead.
        if (zangleInDeg >= ruler.max()):
            lutForce = lut[1:, -1]

        # If the specific zenith angle is smaller than the listed angle range,
        # use the smallest listed zenith angle data instead.
        elif (zangleInDeg <= ruler.min()):
            lutForce = lut[1:, 0]

        # If the specific zenith angle is in the listed angle range,
        # do the linear fit to get the data.
        else:
            # Find the boundary indexes for the specific zenith angle
            p1 = np.where(ruler <= zangleInDeg)[0][-1]
            p2 = p1 + 1

            # Do the linear approximation
            w2 = (zangleInDeg - ruler[p1]) / stepList[p1]
            w1 = 1 - w2

            lutForce = w1 * lut[1:, p1] + w2 * lut[1:, p2]

        return lutForce

    def getActForce(self):
        """Get the mirror actuator forces in N.

        Returns
        ------
        numpy.ndarray
            Actuator forces in N.
        """

        forceInN = self._actForceFile.getMatContent()[:, 3:]

        return forceInN

    def _gridSampInMnInZemax(self,
                             zfInMm,
                             xfInMm,
                             yfInMm,
                             innerRinMm,
                             outerRinMm,
                             nx,
                             ny,
                             resFile=None):
        """Get the grid residue map used in Zemax.

        Parameters
        ----------
        zfInMm : numpy.ndarray
            Surface map in mm.
        xfInMm : numpy.ndarray
            X position in mm.
        yfInMm : numpy.ndarray
            Y position in mm.
        innerRinMm : float
            Inner radius in mm.
        outerRinMm : float
            Outer radius in mm.
        nx : int
            Number of pixel along x-axis of surface residue map. It is noted
            that the real pixel number is nx + 4.
        ny : int
            Number of pixel along y-axis of surface residue map. It is noted
            that the real pixel number is ny + 4.
        resFile : str, optional
            File path to write the surface residue map. (the default is None.)

        Returns
        -------
        str
            Grid residue map related data.
        """

        # Radial basis function approximation/interpolation of surface
        Ff = Rbf(xfInMm, yfInMm, zfInMm)

        # Number of grid points on x-, y-axis.
        # Alway extend 2 points on each side
        # Do not want to cover the edge? change 4->2 on both lines
        NUM_X_PIXELS = nx + 4
        NUM_Y_PIXELS = ny + 4

        # This is spatial extension factor, which is calculated by the slope
        # at edge
        extFx = (NUM_X_PIXELS - 1) / (nx - 1)
        extFy = (NUM_Y_PIXELS - 1) / (ny - 1)
        extFr = np.sqrt(extFx * extFy)

        # Delta x and y
        delx = outerRinMm * 2 * extFx / (NUM_X_PIXELS - 1)
        dely = outerRinMm * 2 * extFy / (NUM_Y_PIXELS - 1)

        # Minimum x and y
        minx = -0.5 * (NUM_X_PIXELS - 1) * delx
        miny = -0.5 * (NUM_Y_PIXELS - 1) * dely

        # Calculate the epsilon
        epsilon = 1e-4 * min(delx, dely)

        # Write four numbers for the header line
        content = "%d %d %.9E %.9E\n" % (NUM_X_PIXELS, NUM_Y_PIXELS, delx,
                                         dely)

        #  Write the rows and columns
        for jj in range(1, NUM_X_PIXELS + 1):
            for ii in range(1, NUM_Y_PIXELS + 1):

                # x and y positions
                x = minx + (ii - 1) * delx
                y = miny + (jj - 1) * dely

                # Invert top to bottom, because Zemax reads (-x,-y) first
                y = -y

                # Calculate the radius
                r = np.sqrt(x**2 + y**2)

                # Set the value as zero when the radius is not between the
                # inner and outer radius.
                if (r < innerRinMm / extFr) or (r > outerRinMm * extFr):

                    z = 0
                    dx = 0
                    dy = 0
                    dxdy = 0

                # Get the value by the fitting
                else:

                    # Get the z
                    z = Ff(x, y)

                    # Compute the dx
                    tem1 = Ff((x + epsilon), y)
                    tem2 = Ff((x - epsilon), y)
                    dx = (tem1 - tem2) / (2.0 * epsilon)

                    # Compute the dy
                    tem1 = Ff(x, (y + epsilon))
                    tem2 = Ff(x, (y - epsilon))
                    dy = (tem1 - tem2) / (2.0 * epsilon)

                    # Compute the dxdy
                    tem1 = Ff((x + epsilon), (y + epsilon))
                    tem2 = Ff((x - epsilon), (y + epsilon))
                    tem3 = (tem1 - tem2) / (2.0 * epsilon)

                    tem1 = Ff((x + epsilon), (y - epsilon))
                    tem2 = Ff((x - epsilon), (y - epsilon))
                    tem4 = (tem1 - tem2) / (2.0 * epsilon)

                    dxdy = (tem3 - tem4) / (2.0 * epsilon)

                content += "%.9E %.9E %.9E %.9E\n" % (z, dx, dy, dxdy)

        # Write the surface residue data into the file
        if (resFile is not None):
            outid = open(resFile, "w")
            outid.write(content)
            outid.close()

        return content

    def _getMirrorResInNormalizedCoor(self, surf, x, y):
        """Get the residue of surface (mirror print along z-axis) after the
        fitting with Zk in the normalized x, y coordinate.

        Parameters
        ----------
        surf : numpy.ndarray
            Mirror surface.
        x : numpy.ndarray
            Normalized x coordinate.
        y : numpy.ndarray
            Normalized y coordinate.

        Returns
        -------
        numpy.ndarray
            Surface residue after the fitting.
        numpy.ndarray
            Fitted Zernike polynomials.
        """

        # Get the surface change along the z-axis in the basis of Zk
        # It is noticed that the x and y coordinates are normalized for the
        # fitting.
        zc = ZernikeFit(surf, x, y, self._numTerms)

        # Residue of fitting
        res = surf - ZernikeEval(zc, x, y)

        return res, zc

    def getPrintthz(self, zAngleInRadian, preCompElevInRadian=0):
        """Get the mirror print in um along z direction in specific zenith
        angle.

        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 along z direction.

        Raises
        ------
        NotImplementedError
            Child class should implemented this.
        """

        raise NotImplementedError("Child class should implemented this.")

    def getTempCorr(self):
        """Get the mirror print correction along z direction for certain
        temperature gradient.

        Returns
        ------
        numpy.ndarray
            Corrected projection along z direction.

        Raises
        ------
        NotImplementedError
            Child class should implemented this.
        """

        raise NotImplementedError("Child class should implemented this.")

    def getMirrorResInMmInZemax(self, writeZcInMnToFilePath=None):
        """Get the residue of surface (mirror print along z-axis) in mm under
        the Zemax coordinate.

        This value is after the fitting with spherical Zernike polynomials
        (zk).

        Parameters
        ----------
        writeZcInMnToFilePath : str, optional
            File path to write the fitted zk in mm. (the default is None.)

        Returns
        ------
        numpy.ndarray
            Fitted residue in mm after removing the fitted zk terms in Zemax
            coordinate.
        numpy.ndarray
            X position in mm in Zemax coordinate.
        numpy.ndarray
            Y position in mm in Zemax coordinate.
        numpy.ndarray
            Fitted zk in mm in Zemax coordinate.

        Raises
        ------
        NotImplementedError
            Child class should implemented this.
        """

        raise NotImplementedError("Child class should implemented this.")

    def writeMirZkAndGridResInZemax(self,
                                    resFile="",
                                    surfaceGridN=200,
                                    writeZcInMnToFilePath=None):
        """Write the grid residue in mm of mirror surface after the fitting
        with Zk under the Zemax coordinate.

        Parameters
        ----------
        resFile : str or list, optional
            File path to save the grid surface residue map. (the default
            is "".)
        surfaceGridN : int, optional
            Surface grid number. (the default is 200.)
        writeZcInMnToFilePath : str, optional
            File path to write the fitted zk in mm. (the default is None.)

        Returns
        ------
        str
            Grid residue map related data.

        Raises
        ------
        NotImplementedError
            Child class should implemented this.
        """

        raise NotImplementedError("Child class should implemented this.")

    def showMirResMap(self, resFile, writeToResMapFilePath=None):
        """Show the mirror residue map.

        Parameters
        ----------
        resFile : str or list
            File path of the grid surface residue map.
        writeToResMapFilePath : str or list, optional
            File path to save the residue map. (the default is None.)

        Raises
        ------
        NotImplementedError
            Child class should implemented this.
        """

        raise NotImplementedError("Child class should implemented this.")
Ejemplo n.º 11
0
class M2Sim(MirrorSim):
    def __init__(self):
        """Initiate the M2 simulator class."""

        # M2 setting file
        configDir = os.path.join(getConfigDir(), "M2")
        settingFilePath = os.path.join(configDir, "m2Setting.yaml")
        self._m2SettingFile = ParamReader(filePath=settingFilePath)

        # Inner and outer radius of M2 mirror in m
        radiusInner = self._m2SettingFile.getSetting("radiusInner")
        radiusOuter = self._m2SettingFile.getSetting("radiusOuter")

        super(M2Sim, self).__init__(radiusInner, radiusOuter, configDir)

        # Mirror surface bending mode grid file
        self._gridFile = ParamReader()

        # Mirror FEA model with gradient temperature data
        self._feaFile = ParamReader()

        self._config("M2_1um_force.yaml", "", "M2_1um_grid.yaml",
                     "M2_GT_FEA.yaml")

    def _config(self, actForceFileName, lutFileName, gridFileName,
                feaFileName):
        """Do the configuration.

        LUT: Look-up table.
        FEA: Finite element analysis.

        Parameters
        ----------
        actForceFileName : str
            Actuator force file name.
        lutFileName : str
            LUT file name.
        gridFileName : str
            File name of bending mode data.
        feaFileName : str
            FEA model data file name.
        """

        numTerms = self._m2SettingFile.getSetting("numTerms")

        super(M2Sim, self).config(numTerms=numTerms,
                                  actForceFileName=actForceFileName,
                                  lutFileName=lutFileName)

        mirrorDataDir = self.getMirrorDataDir()

        gridFilePath = os.path.join(mirrorDataDir, gridFileName)
        self._gridFile.setFilePath(gridFilePath)

        feaFilePath = os.path.join(mirrorDataDir, feaFileName)
        self._feaFile.setFilePath(feaFilePath)

    def getPrintthz(self, zAngleInRadian, preCompElevInRadian=0):
        """Get the mirror print in um 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 um along z direction.
        """

        # Read the FEA file
        data = self._feaFile.getMatContent()

        # Zenith direction in um
        zdz = data[:, 2]

        # Horizon direction in um
        hdz = data[:, 3]

        # Do the M2 gravitational correction.
        # Map the changes of dz on a plane for certain zenith angle
        printthzInUm = zdz * np.cos(zAngleInRadian) + \
            hdz * np.sin(zAngleInRadian)

        # Do the pre-compensation elevation angle correction
        printthzInUm -= zdz * np.cos(preCompElevInRadian) + \
            hdz * np.sin(preCompElevInRadian)

        return printthzInUm

    def getTempCorr(self, m2TzGrad, m2TrGrad):
        """Get the mirror print correction along z direction for certain
        temperature gradient.

        FEA: Finite element analysis.

        Parameters
        ----------
        m2TzGrad : float
            Temperature gradient along z direction in degree C (+/-2sigma
            spans 1C).
        m2TrGrad : float
            Temperature gradient along r direction in degree C (+/-2sigma
            spans 1C).

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

        # Read the FEA file
        data = self._feaFile.getMatContent()

        # Z-gradient in um
        tzdz = data[:, 4]

        # r-gradient in um
        trdz = data[:, 5]

        # Get the temprature correction
        tempCorrInUm = m2TzGrad * tzdz + m2TrGrad * trdz

        return tempCorrInUm

    def getMirrorResInMmInZemax(self, writeZcInMnToFilePath=None):
        """Get the residue of surface (mirror print along z-axis) in mm under
        the Zemax coordinate.

        This value is after the fitting with spherical Zernike polynomials
        (zk).

        Parameters
        ----------
        writeZcInMnToFilePath : str, optional
            File path to write the fitted zk in mm. (the default is None.)

        Returns
        ------
        numpy.ndarray
            Fitted residue in mm after removing the fitted zk terms in Zemax
            coordinate.
        numpy.ndarray
            X position in mm in Zemax coordinate.
        numpy.ndarray
            Y position in mm in Zemax coordinate.
        numpy.ndarray
            Fitted zk in mm in Zemax coordinate.
        """

        # Get the bending mode information
        data = self._gridFile.getMatContent()

        # Get the x, y coordinate
        bx = data[:, 0]
        by = data[:, 1]

        # Transform the M2 coordinate to Zemax coordinate
        bxInZemax, byInZemax, surfInZemax = opt2ZemaxCoorTrans(
            bx, by, self.getSurfAlongZ())

        # Get the mirror residue and zk in um
        RinM = self.getOuterRinM()
        resInUmInZemax, zcInUmInZemax = self._getMirrorResInNormalizedCoor(
            surfInZemax, bxInZemax / RinM, byInZemax / RinM)

        # Change the unit to mm
        resInMmInZemax = resInUmInZemax * 1e-3
        bxInMmInZemax = bxInZemax * 1e3
        byInMmInZemax = byInZemax * 1e3
        zcInMmInZemax = zcInUmInZemax * 1e-3

        # Save the file of fitted Zk
        if (writeZcInMnToFilePath is not None):
            np.savetxt(writeZcInMnToFilePath, zcInMmInZemax)

        return resInMmInZemax, bxInMmInZemax, byInMmInZemax, zcInMmInZemax

    def writeMirZkAndGridResInZemax(self,
                                    resFile="",
                                    surfaceGridN=200,
                                    writeZcInMnToFilePath=None):
        """Write the grid residue in mm of mirror surface after the fitting
        with Zk under the Zemax coordinate.

        Parameters
        ----------
        resFile : str, optional
            File path to save the grid surface residue map. (the default
            is "".)
        surfaceGridN : int, optional
            Surface grid number. (the default is 200.)
        writeZcInMnToFilePath : str, optional
            File path to write the fitted zk in mm. (the default is None.)

        Returns
        -------
        str
            Grid residue map related data.
        """

        # Get the residure map
        resInMmInZemax, bxInMmInZemax, byInMmInZemax = \
            self.getMirrorResInMmInZemax(
                writeZcInMnToFilePath=writeZcInMnToFilePath)[0:3]

        # Change the unit from m to mm
        innerRinMm = self.getInnerRinM() * 1e3
        outerRinMm = self.getOuterRinM() * 1e3

        # Get the residue map used in Zemax
        # Content header: (NUM_X_PIXELS, NUM_Y_PIXELS, delta x, delta y)
        # Content: (z, dx, dy, dxdy)
        content = self._gridSampInMnInZemax(resInMmInZemax,
                                            bxInMmInZemax,
                                            byInMmInZemax,
                                            innerRinMm,
                                            outerRinMm,
                                            surfaceGridN,
                                            surfaceGridN,
                                            resFile=resFile)

        return content

    def showMirResMap(self, resFile, writeToResMapFilePath=None):
        """Show the mirror residue map.

        Parameters
        ----------
        resFile : str
            File path of the grid surface residue map.
        writeToResMapFilePath : str, optional
            File path to save the residue map. (the default is None.)
        """

        # Get the residure map
        resInMmInZemax, bxInMmInZemax, byInMmInZemax = \
            self.getMirrorResInMmInZemax()[0:3]

        # Change the unit
        outerRinMm = self.getOuterRinM() * 1e3
        plotResMap(resInMmInZemax,
                   bxInMmInZemax,
                   byInMmInZemax,
                   outerRinMm,
                   resFile=resFile,
                   writeToResMapFilePath=writeToResMapFilePath)