def testZernikeAnnularGradDxy(self): surfGrad = ZernikeAnnularGrad( self.zerCoef, self.xx, self.yy, self.obscuration, "dxy" ) self._checkAnsWithFile(surfGrad, "annularZernikeGradDxy.txt")
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 __solvePoissonEq(self, inst, I1, I2, iOutItr=0): """ Solve the Poisson's equation by Fourier transform (differential) or serial expansion (integration). There is no convergence for fft actually. Need to add the difference comparison and Xa method. Need to discuss further for this. Arguments: inst {[Instrument]} -- Instrument to use. I1 {[Image]} -- Intra- or extra-focal image. I2 {[Image]} -- Intra- or extra-focal image. Keyword Arguments: iOutItr {[int]} -- ith number of outer loop iteration which is important in "fft" algorithm (default: {0}). Returns: [float] -- Coefficients of normal/ annular Zernike polynomials. [float] -- Estimated wavefront. """ # Calculate the aperature pixel size apertureDiameter = inst.parameter["apertureDiameter"] sensorFactor = inst.parameter["sensorFactor"] sensorSamples = inst.parameter["sensorSamples"] aperturePixelSize = apertureDiameter * sensorFactor / sensorSamples # Calculate the differential Omega dOmega = aperturePixelSize**2 # Solve the Poisson's equation based on the type of algorithm numTerms = self.parameter["numTerms"] zobsR = self.parameter["zobsR"] PoissonSolver = self.parameter["PoissonSolver"] if (PoissonSolver == "fft"): # Use the differential method by fft to solve the Poisson's equation # Parameter to determine the threshold of calculating I0. sumclipSequence = self.parameter["sumclipSequence"] cliplevel = sumclipSequence[iOutItr] # Generate the v, u-coordinates on pupil plane padDim = self.parameter["padDim"] v, u = np.mgrid[-0.5 / aperturePixelSize:0.5 / aperturePixelSize:1. / padDim / aperturePixelSize, -0.5 / aperturePixelSize:0.5 / aperturePixelSize:1. / padDim / aperturePixelSize] # Show the threshold and pupil coordinate information if (self.debugLevel >= 3): print("iOuter=%d, cliplevel=%4.2f" % (iOutItr, cliplevel)) print(v.shape) # Calculate the const of fft: FT{Delta W} = -4*pi^2*(u^2+v^2) * FT{W} u2v2 = -4 * (np.pi**2) * (u * u + v * v) # Set origin to Inf to result in 0 at origin after filtering ctrIdx = int(np.floor(padDim / 2.0)) u2v2[ctrIdx, ctrIdx] = np.inf # Calculate the wavefront signal Sini = self.__createSignal(inst, I1, I2, cliplevel) # Find the just-outside and just-inside indices of a ring in pixels # This is for the use in setting dWdn = 0 boundaryT = self.parameter["boundaryT"] struct = generate_binary_structure(2, 1) struct = iterate_structure(struct, boundaryT) ApringOut = np.logical_xor( binary_dilation(self.pMask, structure=struct), self.pMask).astype(int) ApringIn = np.logical_xor( binary_erosion(self.pMask, structure=struct), self.pMask).astype(int) bordery, borderx = np.nonzero(ApringOut) # Put the signal in boundary (since there's no existing Sestimate, S just equals self.S # as the initial condition of SCF S = Sini.copy() for jj in range(int(self.parameter["innerItr"])): # Calculate FT{S} SFFT = np.fft.fftshift(np.fft.fft2(np.fft.fftshift(S))) # Calculate W by W=IFT{ FT{S}/(-4*pi^2*(u^2+v^2)) } W = np.fft.fftshift( np.fft.irfft2(np.fft.fftshift(SFFT / u2v2), s=S.shape)) # Estimate the wavefront (includes zeroing offset & masking to the aperture size) # Take the estimated wavefront West = extractArray(W, sensorSamples) # Calculate the offset offset = West[self.pMask == 1].mean() West = West - offset West[self.pMask == 0] = 0 # Set dWestimate/dn = 0 around boundary WestdWdn0 = West.copy() # Do a 3x3 average around each border pixel, including only those pixels # inside the aperture for ii in range(len(borderx)): reg = West[borderx[ii] - boundaryT:borderx[ii] + boundaryT + 1, bordery[ii] - boundaryT:bordery[ii] + boundaryT + 1] intersectIdx = ApringIn[borderx[ii] - boundaryT:borderx[ii] + boundaryT + 1, bordery[ii] - boundaryT:bordery[ii] + boundaryT + 1] WestdWdn0[borderx[ii], bordery[ii]] = reg[np.nonzero( intersectIdx)].mean() # Take Laplacian to find sensor signal estimate (Delta W = S) del2W = laplace(WestdWdn0) / dOmega # Extend the dimension of signal to the order of 2 for "fft" to use Sest = padArray(del2W, padDim) # Put signal back inside boundary, leaving the rest of Sestimate Sest[self.pMaskPad == 1] = Sini[self.pMaskPad == 1] # Need to recheck this condition S = Sest # Define the estimated wavefront # self.West = West.copy() # Calculate the coefficient of normal/ annular Zernike polynomials if (self.parameter["compMode"] == "zer"): zc = ZernikeMaskedFit(West, inst.xSensor, inst.ySensor, numTerms, self.pMask, zobsR) else: zc = np.zeros(numTerms) elif (PoissonSolver == "exp"): # Use the integration method by serial expansion to solve the Poisson's equation # Calculate I0 and dI I0, dI = self.__getdIandI(I1, I2) # Get the x, y coordinate in mask. The element outside mask is 0. xSensor = inst.xSensor * self.cMask ySensor = inst.ySensor * self.cMask # Create the F matrix and Zernike-related matrixes F = np.zeros(numTerms) dZidx = np.zeros([numTerms, sensorSamples, sensorSamples]) dZidy = dZidx.copy() zcCol = np.zeros(numTerms) for ii in range(int(numTerms)): # Calculate the matrix for each Zk related component # Set the specific Zk cofficient to be 1 for the calculation zcCol[ii] = 1 F[ii] = np.sum(dI * ZernikeAnnularEval(zcCol, xSensor, ySensor, zobsR)) * dOmega dZidx[ii, :, :] = ZernikeAnnularGrad(zcCol, xSensor, ySensor, zobsR, "dx") dZidy[ii, :, :] = ZernikeAnnularGrad(zcCol, xSensor, ySensor, zobsR, "dy") # Set the specific Zk cofficient back to 0 to avoid interfering other Zk's calculation zcCol[ii] = 0 # Calculate Mij matrix, need to check the stability of integration and symmetry later Mij = np.zeros([numTerms, numTerms]) for ii in range(numTerms): for jj in range(numTerms): Mij[ii, jj] = np.sum(I0 * ( dZidx[ii, :, :].squeeze() * dZidx[jj, :, :].squeeze() + dZidy[ii, :, :].squeeze() * dZidy[jj, :, :].squeeze())) Mij = dOmega / (apertureDiameter / 2.)**2 * Mij # Calculate dz focalLength = inst.parameter["focalLength"] offset = inst.parameter["offset"] dz = 2 * focalLength * (focalLength - offset) / offset # Define zc zc = np.zeros(numTerms) # Consider specific Zk terms only idx = [x - 1 for x in self.parameter["ZTerms"]] # Solve the equation: M*W = F => W = M^(-1)*F zc_tmp = np.linalg.lstsq(Mij[:, idx][idx], F[idx], rcond=None)[0] / dz zc[idx] = zc_tmp # Estimate the wavefront surface based on z4 - z22 # z0 - z3 are set to be 0 instead West = ZernikeAnnularEval(np.concatenate(([0, 0, 0], zc[3:])), xSensor, ySensor, zobsR) return zc, West
def __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
def testZernikeAnnularGradWrongAxis(self): with self.assertRaises(ValueError): ZernikeAnnularGrad( self.zerCoef, self.xx, self.yy, self.obscuration, "wrongAxis" )