Example #1
0
    def testZernikeAnnularJacobian2nd(self):

        annuZerJacobian = ZernikeAnnularJacobian(
            self.zerCoef, self.xx, self.yy, self.obscuration, "2nd"
        )

        self._checkAnsWithFile(annuZerJacobian, "annularZernikeJaco2nd.txt")
Example #2
0
    def _aperture2image(self, inst, algo, zcCol, lutx, luty, projSamples,
                        model):
        """Calculate the x, y-coordinate on the focal plane and the related
        Jacobian matrix.

        Parameters
        ----------
        inst : Instrument
            Instrument to use.
        algo : Algorithm
            Algorithm to solve the Poisson's equation. It can by done by the
            fast Fourier transform or serial expansion.
        zcCol : numpy.ndarray
            Coefficients of optical basis. It is Zernike polynomials in the
            baseline.
        lutx : numpy.ndarray
            X-coordinate on pupil plane.
        luty : numpy.ndarray
            Y-coordinate on pupil plane.
        projSamples : int
            Dimension of projected image. This value considers the
            magnification ratio of donut image.
        model : str
            Optical model. It can be "paraxial", "onAxis", or "offAxis".

        Returns
        -------
        numpy.ndarray
            X coordinate on the focal plane.
        numpy.ndarray
            Y coordinate on the focal plane.
        numpy.ndarray
            Jacobian matrix between the pupil and focal plane.
        """

        # Get the radius: R = D/2
        R = inst.getApertureDiameter() / 2

        # Calculate C = -f(f-l)/l/R^2. This is for the calculation of reduced
        # coordinate.
        defocalDisOffset = inst.getDefocalDisOffset()
        if self.defocalType == DefocalType.Intra:
            l = defocalDisOffset
        elif self.defocalType == DefocalType.Extra:
            l = -defocalDisOffset

        focalLength = inst.getFocalLength()
        myC = -focalLength * (focalLength - l) / l / R**2

        # Get the functions to do the off-axis correction by numerical fitting
        # Order to do the off-axis correction. The order is 10 now.
        offAxisPolyOrder = algo.getOffAxisPolyOrder()
        polyFunc = self._getFunction("poly%d_2D" % offAxisPolyOrder)
        polyGradFunc = self._getFunction("poly%dGrad" % offAxisPolyOrder)

        # Calculate the distance to center
        lutr = np.sqrt(lutx**2 + luty**2)

        # Calculated the extended ring radius (delta r), which is to extended
        # the available pupil area.
        # 1 pixel larger than projected pupil. No need to be EF-like, anything
        # outside of this will be masked off by the computational mask
        sensorFactor = inst.getSensorFactor()
        onepixel = 1 / (projSamples / 2 / sensorFactor)

        # Get the index that the point is out of the range of extended pupil
        obscuration = inst.getObscuration()
        idxout = (lutr > 1 + onepixel) | (lutr < obscuration - onepixel)

        # Define the element to be NaN if it is out of range
        lutx[idxout] = np.nan
        luty[idxout] = np.nan

        # Get the index in the extended area of outer boundary with the width
        # of onepixel
        idxbound = (lutr <= 1 + onepixel) & (lutr > 1)

        # Calculate the extended x, y-coordinate (x' = x/r*r', r'=1)
        lutx[idxbound] = lutx[idxbound] / lutr[idxbound]
        luty[idxbound] = luty[idxbound] / lutr[idxbound]

        # Get the index in the extended area of inner boundary with the width
        # of onepixel
        idxinbd = (lutr < obscuration) & (lutr > obscuration - onepixel)

        # Calculate the extended x, y-coordinate (x' = x/r*r', r'=obscuration)
        lutx[idxinbd] = lutx[idxinbd] / lutr[idxinbd] * obscuration
        luty[idxinbd] = luty[idxinbd] / lutr[idxinbd] * obscuration

        # Get the corrected x, y-coordinate on focal plane (lutxp, lutyp)
        if model == "paraxial":
            # No correction is needed in "paraxial" model
            lutxp = lutx
            lutyp = luty

        elif model == "onAxis":

            # Calculate F(x, y) = m * sqrt(f^2-R^2) / sqrt(f^2-(x^2+y^2)*R^2)
            # m is the mask scaling factor
            myA2 = (focalLength**2 - R**2) / (focalLength**2 - lutr**2 * R**2)

            # Put the unphysical value as NaN
            myA = myA2.copy()
            idx = myA < 0
            myA[idx] = np.nan
            myA[~idx] = np.sqrt(myA2[~idx])

            # Mask scaling factor (for fast beam)
            maskScalingFactor = algo.getMaskScalingFactor()

            # Calculate the x, y-coordinate on focal plane
            # x' = F(x,y)*x + C*(dW/dx), y' = F(x,y)*y + C*(dW/dy)
            lutxp = maskScalingFactor * myA * lutx
            lutyp = maskScalingFactor * myA * luty

        elif model == "offAxis":

            # Get the coefficient of polynomials for off-axis correction
            tt = self.offAxisOffset

            cx = (self.offAxisCoeff[0, :] - self.offAxisCoeff[2, :]) * (
                tt + l) / (2 * tt) + self.offAxisCoeff[2, :]
            cy = (self.offAxisCoeff[1, :] - self.offAxisCoeff[3, :]) * (
                tt + l) / (2 * tt) + self.offAxisCoeff[3, :]

            # This will be inverted back by typesign later on.
            # We do the inversion here to make the (x,y)->(x',y') equations has
            # the same form as the paraxial case.
            cx = np.sign(l) * cx
            cy = np.sign(l) * cy

            # Do the orthogonalization: x'=1/sqrt(2)*(x+y), y'=1/sqrt(2)*(x-y)
            # Calculate the rotation angle for the orthogonalization
            fieldDist = self._getFieldDistFromOrigin()
            costheta = (self.fieldX + self.fieldY) / fieldDist / np.sqrt(2)
            if costheta > 1:
                costheta = 1
            elif costheta < -1:
                costheta = -1

            sintheta = np.sqrt(1 - costheta**2)
            if self.fieldY < self.fieldX:
                sintheta = -sintheta

            # Create the pupil grid in off-axis model. This gives the
            # x,y-coordinate in the extended ring area defined by the parameter
            # of onepixel.

            # Get the mask-related parameters
            maskCa, maskRa, maskCb, maskRb = self._interpMaskParam(
                self.fieldX, self.fieldY, inst.getMaskOffAxisCorr())

            lutx, luty = self._createPupilGrid(
                lutx,
                luty,
                onepixel,
                maskCa,
                maskCb,
                maskRa,
                maskRb,
                self.fieldX,
                self.fieldY,
            )

            # Calculate the x, y-coordinate on focal plane

            # First rotate back to reference orientation
            lutx0 = lutx * costheta + luty * sintheta
            luty0 = -lutx * sintheta + luty * costheta

            # Use the mapping at reference orientation
            lutxp0 = polyFunc(cx, lutx0, y=luty0)
            lutyp0 = polyFunc(cy, lutx0, y=luty0)

            # Rotate back to focal plane
            lutxp = lutxp0 * costheta - lutyp0 * sintheta
            lutyp = lutxp0 * sintheta + lutyp0 * costheta

            # Zemax data are in mm, therefore 1000
            dimOfDonut = inst.getDimOfDonutOnSensor()
            pixelSize = inst.getCamPixelSize()
            reduced_coordi_factor = 1e-3 / (dimOfDonut / 2 * pixelSize /
                                            sensorFactor)

            # Reduced coordinates, so that this can be added with the dW/dz
            lutxp = lutxp * reduced_coordi_factor
            lutyp = lutyp * reduced_coordi_factor

        else:
            print("Wrong optical model type in compensate. \n")
            return

        # Obscuration of annular aperture
        zobsR = algo.getObsOfZernikes()

        # Calculate the x, y-coordinate on focal plane
        # x' = F(x,y)*x + C*(dW/dx), y' = F(x,y)*y + C*(dW/dy)

        # In Model basis (zer: Zernike polynomials)
        if zcCol.ndim == 1:
            lutxp = lutxp + myC * ZernikeAnnularGrad(zcCol, lutx, luty, zobsR,
                                                     "dx")
            lutyp = lutyp + myC * ZernikeAnnularGrad(zcCol, lutx, luty, zobsR,
                                                     "dy")

        # Make the sign to be consistent
        if self.defocalType == DefocalType.Extra:
            lutxp = -lutxp
            lutyp = -lutyp

        # Calculate the Jacobian matrix
        # In Model basis (zer: Zernike polynomials)
        if zcCol.ndim == 1:
            if model == "paraxial":
                J = (1 + myC *
                     ZernikeAnnularJacobian(zcCol, lutx, luty, zobsR, "1st") +
                     myC**2 *
                     ZernikeAnnularJacobian(zcCol, lutx, luty, zobsR, "2nd"))

            elif model == "onAxis":
                xpox = maskScalingFactor * myA * (
                    1 + lutx**2 * R**2.0 / (focalLength**2 - R**2 * lutr**2)
                ) + myC * ZernikeAnnularGrad(zcCol, lutx, luty, zobsR, "dx2")

                ypoy = maskScalingFactor * myA * (
                    1 + luty**2 * R**2.0 / (focalLength**2 - R**2 * lutr**2)
                ) + myC * ZernikeAnnularGrad(zcCol, lutx, luty, zobsR, "dy2")

                xpoy = maskScalingFactor * myA * lutx * luty * R**2 / (
                    focalLength**2 - R**2 * lutr**2
                ) + myC * ZernikeAnnularGrad(zcCol, lutx, luty, zobsR, "dxy")

                ypox = xpoy

                J = xpox * ypoy - xpoy * ypox

            elif model == "offAxis":
                xp0ox = (polyGradFunc(cx, lutx0, luty0, "dx") * costheta -
                         polyGradFunc(cx, lutx0, luty0, "dy") * sintheta)

                yp0ox = (polyGradFunc(cy, lutx0, luty0, "dx") * costheta -
                         polyGradFunc(cy, lutx0, luty0, "dy") * sintheta)

                xp0oy = (polyGradFunc(cx, lutx0, luty0, "dx") * sintheta +
                         polyGradFunc(cx, lutx0, luty0, "dy") * costheta)

                yp0oy = (polyGradFunc(cy, lutx0, luty0, "dx") * sintheta +
                         polyGradFunc(cy, lutx0, luty0, "dy") * costheta)

                xpox = (xp0ox * costheta - yp0ox * sintheta
                        ) * reduced_coordi_factor + myC * ZernikeAnnularGrad(
                            zcCol, lutx, luty, zobsR, "dx2")

                ypoy = (xp0oy * sintheta + yp0oy * costheta
                        ) * reduced_coordi_factor + myC * ZernikeAnnularGrad(
                            zcCol, lutx, luty, zobsR, "dy2")

                temp = myC * ZernikeAnnularGrad(zcCol, lutx, luty, zobsR,
                                                "dxy")

                # if temp==0,xpoy doesn't need to be symmetric about x=y
                xpoy = (xp0oy * costheta -
                        yp0oy * sintheta) * reduced_coordi_factor + temp

                # xpoy-flipud(rot90(ypox))==0 is true
                ypox = (xp0ox * sintheta +
                        yp0ox * costheta) * reduced_coordi_factor + temp

                J = xpox * ypoy - xpoy * ypox

        return lutxp, lutyp, J
    def __aperture2image(self, inst, algo, zcCol, lutx, luty, projSamples,
                         model):
        """
        
        Calculate the x, y-coordinate on the focal plane and the related Jacobian matrix.
        
        Arguments:
            inst {[Instrument]} -- Instrument to use.
            algo {[Algorithm]} -- Algorithm to solve the Poisson's equation. It can by done 
                                  by the fast Fourier transform or serial expansion.
            zcCol {[float]} -- Coefficients of optical basis. It is Zernike polynomials in the 
                               baseline.
            lutx {[float]} -- x-coordinate on pupil plane.
            luty {[float]} -- y-coordinate on pupil plane.
            projSamples {[int]} -- Dimension of projected image. This value considers the
                                   magnification ratio of donut image.
            model {[string]} -- Optical model. It can be "paraxial", "onAxis", or "offAxis".
        
        Returns:
            [float] -- x, y-coordinate on the focal plane.
            [float] -- Jacobian matrix between the pupil and focal plane.
        """

        # Get the radius: R = D/2
        R = inst.parameter["apertureDiameter"] / 2.0

        # Calculate C = -f(f-l)/l/R^2. This is for the calculation of reduced coordinate.
        if (self.atype == self.INTRA):
            l = inst.parameter["offset"]
        elif (self.atype == self.EXTRA):
            l = -inst.parameter["offset"]
        focalLength = inst.parameter["focalLength"]
        myC = -focalLength * (focalLength - l) / l / R**2

        # Get the functions to do the off-axis correction by numerical fitting
        # Order to do the off-axis correction. The order is 10 now.
        offAxisPolyOrder = algo.parameter["offAxisPolyOrder"]
        polyFunc = self.__getFunction("poly%d_2D" % offAxisPolyOrder)
        polyGradFunc = self.__getFunction("poly%dGrad" % offAxisPolyOrder)

        # Calculate the distance to center
        lutr = np.sqrt(lutx**2 + luty**2)

        # Calculated the extended ring radius (delta r), which is to extended the available
        # pupil area.
        # 1 pixel larger than projected pupil. No need to be EF-like, anything
        # outside of this will be masked off by the computational mask
        sensorFactor = inst.parameter["sensorFactor"]
        onepixel = 1 / (projSamples / 2 / sensorFactor)

        # Get the index that the point is out of the range of extended pupil
        obscuration = inst.parameter["obscuration"]
        idxout = (lutr > 1 + onepixel) | (lutr < obscuration - onepixel)

        # Define the element to be NaN if it is out of range
        lutx[idxout] = np.nan
        luty[idxout] = np.nan

        # Get the index in the extended area of outer boundary with the width of onepixel
        idxbound = (lutr <= 1 + onepixel) & (lutr > 1)

        # Calculate the extended x, y-coordinate (x' = x/r*r', r'=1)
        lutx[idxbound] = lutx[idxbound] / lutr[idxbound]
        luty[idxbound] = luty[idxbound] / lutr[idxbound]

        # Get the index in the extended area of inner boundary with the width of onepixel
        idxinbd = (lutr < obscuration) & (lutr > obscuration - onepixel)

        # Calculate the extended x, y-coordinate (x' = x/r*r', r'=obscuration)
        lutx[idxinbd] = lutx[idxinbd] / lutr[idxinbd] * obscuration
        luty[idxinbd] = luty[idxinbd] / lutr[idxinbd] * obscuration

        # Get the corrected x, y-coordinate on focal plane (lutxp, lutyp)
        if (model == "paraxial"):
            # No correction is needed in "paraxial" model
            lutxp = lutx
            lutyp = luty

        elif (model == "onAxis"):

            # Calculate F(x, y) = m * sqrt(f^2-R^2) / sqrt(f^2-(x^2+y^2)*R^2)
            # m is the mask scaling factor
            myA2 = (focalLength**2 - R**2) / (focalLength**2 - lutr**2 * R**2)

            # Put the unphysical value as NaN
            myA = myA2.copy()
            idx = (myA < 0)
            myA[idx] = np.nan
            myA[~idx] = np.sqrt(myA2[~idx])

            # Mask scaling factor (for fast beam)
            maskScalingFactor = algo.parameter["maskScalingFactor"]

            # Calculate the x, y-coordinate on focal plane
            # x' = F(x,y)*x + C*(dW/dx), y' = F(x,y)*y + C*(dW/dy)
            lutxp = maskScalingFactor * myA * lutx
            lutyp = maskScalingFactor * myA * luty

        elif (model == "offAxis"):

            # Get the coefficient of polynomials for off-axis correction
            tt = self.offAxisOffset

            cx = (self.offAxis_coeff[0, :] - self.offAxis_coeff[2, :]) * (tt+l)/(2*tt) + \
                    self.offAxis_coeff[2, :]
            cy = (self.offAxis_coeff[1, :] - self.offAxis_coeff[3, :]) * (tt+l)/(2*tt) + \
                    self.offAxis_coeff[3, :]

            # This will be inverted back by typesign later on.
            # We do the inversion here to make the (x,y)->(x',y') equations has
            # the same form as the paraxial case.
            cx = np.sign(l) * cx
            cy = np.sign(l) * cy

            # Do the orthogonalization: x'=1/sqrt(2)*(x+y), y'=1/sqrt(2)*(x-y)
            # Calculate the rotation angle for the orthogonalization
            costheta = (self.fieldX + self.fieldY) / self.fldr / np.sqrt(2)
            if (costheta > 1):
                costheta = 1
            elif (costheta < -1):
                costheta = -1

            sintheta = np.sqrt(1 - costheta**2)
            if (self.fieldY < self.fieldX):
                sintheta = -sintheta

            # Create the pupil grid in off-axis model. This gives the x,y-coordinate
            # in the extended ring area defined by the parameter of onepixel.

            # Get the mask-related parameters
            maskCa, maskRa, maskCb, maskRb = self.__interpMaskParam(
                self.fieldX, self.fieldY, inst.maskParam)

            lutx, luty = self.__createPupilGrid(lutx, luty, onepixel, maskCa,
                                                maskCb, maskRa, maskRb,
                                                self.fieldX, self.fieldY)

            # Calculate the x, y-coordinate on focal plane

            # First rotate back to reference orientation
            lutx0 = lutx * costheta + luty * sintheta
            luty0 = -lutx * sintheta + luty * costheta

            # Use the mapping at reference orientation
            lutxp0 = polyFunc(cx, lutx0, y=luty0)
            lutyp0 = polyFunc(cy, lutx0, y=luty0)

            # Rotate back to focal plane
            lutxp = lutxp0 * costheta - lutyp0 * sintheta
            lutyp = lutxp0 * sintheta + lutyp0 * costheta

            # Zemax data are in mm, therefore 1000
            sensorSamples = inst.parameter["sensorSamples"]
            pixelSize = inst.parameter["pixelSize"]
            reduced_coordi_factor = 1e-3 / (sensorSamples / 2 * pixelSize /
                                            sensorFactor)

            # Reduced coordinates, so that this can be added with the dW/dz
            lutxp = lutxp * reduced_coordi_factor
            lutyp = lutyp * reduced_coordi_factor

        else:
            print('Wrong optical model type in compensate. \n')
            return

        # Obscuration of annular aperture
        zobsR = algo.parameter["zobsR"]

        # Calculate the x, y-coordinate on focal plane
        # x' = F(x,y)*x + C*(dW/dx), y' = F(x,y)*y + C*(dW/dy)

        # In Model basis (zer: Zernike polynomials)
        if (zcCol.ndim == 1):
            lutxp = lutxp + myC * ZernikeAnnularGrad(zcCol, lutx, luty, zobsR,
                                                     "dx")
            lutyp = lutyp + myC * ZernikeAnnularGrad(zcCol, lutx, luty, zobsR,
                                                     "dy")

        # Make the sign to be consistent
        if (self.atype == "extra"):
            lutxp = -lutxp
            lutyp = -lutyp

        # Calculate the Jacobian matrix
        # In Model basis (zer: Zernike polynomials)
        if (zcCol.ndim == 1):
            if (model == "paraxial"):
                J = 1 + myC * ZernikeAnnularJacobian(zcCol, lutx, luty, zobsR, "1st") + \
                    myC**2 * ZernikeAnnularJacobian(zcCol, lutx, luty, zobsR, "2nd")

            elif (model == "onAxis"):
                xpox = maskScalingFactor * myA * (1 + \
                    lutx**2 * R**2. / (focalLength**2 - R**2 * lutr**2)) + \
                    myC * ZernikeAnnularGrad(zcCol, lutx, luty, zobsR, "dx2")

                ypoy = maskScalingFactor * myA * (1 + \
                    luty**2 * R**2. / (focalLength**2 - R**2 * lutr**2)) + \
                    myC * ZernikeAnnularGrad(zcCol, lutx, luty, zobsR, "dy2")

                xpoy = maskScalingFactor * myA * \
                    lutx * luty * R**2 / (focalLength**2 - R**2 * lutr**2) + \
                    myC * ZernikeAnnularGrad(zcCol, lutx, luty, zobsR, "dxy")

                ypox = xpoy

                J = xpox * ypoy - xpoy * ypox

            elif (model == "offAxis"):
                xp0ox = polyGradFunc(cx, lutx0, luty0, "dx") * costheta - \
                        polyGradFunc(cx, lutx0, luty0, "dy") * sintheta

                yp0ox = polyGradFunc(cy, lutx0, luty0, "dx") * costheta - \
                        polyGradFunc(cy, lutx0, luty0, "dy") * sintheta

                xp0oy = polyGradFunc(cx, lutx0, luty0, "dx") * sintheta + \
                        polyGradFunc(cx, lutx0, luty0, "dy") * costheta

                yp0oy = polyGradFunc(cy, lutx0, luty0, "dx") * sintheta + \
                        polyGradFunc(cy, lutx0, luty0, "dy") * costheta

                xpox = (xp0ox*costheta - yp0ox*sintheta)*reduced_coordi_factor + \
                        myC*ZernikeAnnularGrad(zcCol, lutx, luty, zobsR, "dx2")

                ypoy = (xp0oy*sintheta + yp0oy*costheta)*reduced_coordi_factor + \
                        myC*ZernikeAnnularGrad(zcCol, lutx, luty, zobsR, "dy2")

                temp = myC * ZernikeAnnularGrad(zcCol, lutx, luty, zobsR,
                                                "dxy")

                # if temp==0,xpoy doesn't need to be symmetric about x=y
                xpoy = (xp0oy * costheta -
                        yp0oy * sintheta) * reduced_coordi_factor + temp

                # xpoy-flipud(rot90(ypox))==0 is true
                ypox = (xp0ox * sintheta +
                        yp0ox * costheta) * reduced_coordi_factor + temp

                J = xpox * ypoy - xpoy * ypox

        return lutxp, lutyp, J
Example #4
0
    def testZernikeAnnularJacobianWrongType(self):

        with self.assertRaises(ValueError):
            ZernikeAnnularJacobian(
                self.zerCoef, self.xx, self.yy, self.obscuration, "wrongType"
            )