def _genOrthogonalFunc(self, r, theta, z1, z2, e): func = ( r * ZernikeAnnularEval(z1, r * np.cos(theta), r * np.sin(theta), e) * ZernikeAnnularEval(z2, r * np.cos(theta), r * np.sin(theta), e)) return func
def createZernikeList(nz, x, y, e): '''Create a list of masked zernike images''' zimages = [] for i in range(nz): z = np.zeros(nz) z[i] = 1. zimages.append(ZernikeAnnularEval(z, x, y, e)) return zimages
def testZernikeMaskFit(self): e = 0.2 nc = 6 surface = ZernikeAnnularEval(self.zerCoef[0:nc], self.xx, self.yy, e) # mask data cut = -0.9 r = np.sqrt(self.xx**2 + self.yy**2) idx = (r > 1) | (r < e) | (self.xx < cut) xx = self.xx[:].copy() yy = self.yy[:].copy() xx[idx] = np.nan yy[idx] = np.nan mask = ~np.isnan(xx) zr = ZernikeMaskedFit(surface, xx, yy, nc, mask, e) self.assertLess(np.sum(np.abs(zr - self.zerCoef[0:nc]) ** 2), 1e-10)
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 getPrintthz(self, zAngleInRadian, preCompElevInRadian=0, FEAzenFileName="M1M3_dxdydz_zenith.txt", FEAhorFileName="M1M3_dxdydz_horizon.txt", gridFileName="M1M3_1um_156_grid.DAT"): """ Get the mirror print in m along z direction in specific zenith angle. Arguments: zAngleInRadian {[float]} -- Zenith angle in radian. Keyword Arguments: preCompElevInRadian {float} -- Pre-compensation elevation angle in radian. (default: {0}) FEAzenFileName {str} -- Finite element analysis (FEA) model data file name in zenith angle. (default: {"M1M3_dxdydz_zenith.txt"}) FEAhorFileName {str} -- Finite element analysis (FEA) model data file name in horizon angle. (default: {"M1M3_dxdydz_horizon.txt"}) gridFileName {str} -- File name of bending mode data. (default: {"M1M3_1um_156_grid.DAT"}) Returns: [ndarray] -- Corrected projection in m along z direction. """ # Data needed to determine gravitational print through data = self.getMirrorData(FEAzenFileName) zdx = data[:, 0] zdy = data[:, 1] zdz = data[:, 2] data = self.getMirrorData(FEAhorFileName) hdx = data[:, 0] hdy = data[:, 1] hdz = data[:, 2] # Do the M1M3 gravitational correction. # Map the changes of dx, dy, and dz on a plane for certain zenith angle printthxInM = zdx * np.cos(zAngleInRadian) + hdx * np.sin( zAngleInRadian) printthyInM = zdy * np.cos(zAngleInRadian) + hdy * np.sin( zAngleInRadian) printthzInM = zdz * np.cos(zAngleInRadian) + hdz * np.sin( zAngleInRadian) # Get the bending mode information idx1, idx3, bx, by, bz = self.__getMirCoor(gridFileName=gridFileName) # Calcualte the mirror ideal shape zRef = self.__idealShape(bx * 1000, by * 1000, idx1, idx3) / 1000 # Calcualte the mirror ideal shape with the displacement zpRef = self.__idealShape((bx + printthxInM) * 1000, (by + printthyInM) * 1000, idx1, idx3) / 1000 # Convert printthz into surface sag to get the estimated wavefront error # Do the zenith angle correction by the linear approximation with the ideal shape printthzInM = printthzInM - (zpRef - zRef) # Normalize the coordinate R = self.RinM[0] Ri = self.RiInM[0] normX = bx / R normY = by / R obs = Ri / R # Fit the annular Zernike polynomials z0-z2 (piton, x-tilt, y-tilt) zc = ZernikeAnnularFit(printthzInM, normX, normY, 3, obs) # Do the estimated wavefront error correction for the mirror projection printthzInM -= ZernikeAnnularEval(zc, normX, normY, obs) return printthzInM
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 __singleItr(self, inst, I1, I2, model, tol=1e-3): """ Run the outer-loop with single iteration to solve the transport of intensity equation (TIE). This is to compensate the approximation of wavefront S = -1/(delta Z) * (I1 - I2)/ (I1 + I2)). Arguments: inst {[Instrument]} -- Instrument to use. I1 {[Image]} -- Intra- or extra-focal image. I2 {[Image]} -- Intra- or extra-focal image. model {[string]} -- Optical model. It can be "paraxial", "onAxis", or "offAxis". tol {[float]} -- Tolerance of difference of coefficients of Zk polynomials compared with the previours iteration. """ # Use the zonal mode ("zer") compMode = self.parameter["compMode"] # Define the gain of feedbackGain feedbackGain = self.parameter["feedbackGain"] # Set the pre-condition if (self.currentItr == 0): # Check this is the first time of running iteration or not if (I1.image0 is None or I2.image0 is None): # Check the image dimension if (I1.image.shape != I2.image.shape): print( "Error: The intra and extra image stamps need to be of same size." ) sys.exit() # Calculate the pupil mask (binary matrix) and related parameters I1.makeMask(inst, model, self.parameter["boundaryT"], 1) I2.makeMask(inst, model, self.parameter["boundaryT"], 1) self.__makeMasterMask(I1, I2, self.parameter["PoissonSolver"]) # Load the offAxis correction coefficients if (model == "offAxis"): instDir = os.path.join(inst.instDir, inst.instName) I1.getOffAxisCorr(instDir, self.parameter["offAxisPolyOrder"]) I2.getOffAxisCorr(instDir, self.parameter["offAxisPolyOrder"]) # Cocenter the images to the center referenced to fieldX and fieldY. Need to check the # availability of this. I1.imageCoCenter(inst, debugLevel=self.debugLevel) I2.imageCoCenter(inst, debugLevel=self.debugLevel) # Update the self-initial image I1.updateImage0() I2.updateImage0() # Initialize the variables used in the iteration. self.zcomp = np.zeros(self.parameter["numTerms"]) self.zc = self.zcomp.copy() sensorSamples = inst.parameter["sensorSamples"] self.wcomp = np.zeros([sensorSamples, sensorSamples]) self.West = self.wcomp.copy() self.caustic = False # Rename this index (currentItr) for the simplification jj = self.currentItr # Solve the transport of intensity equation (TIE) if (not self.caustic): # Reset the images before the compensation I1.updateImage(I1.image0.copy()) I2.updateImage(I2.image0.copy()) if (compMode == "zer"): # Zk coefficient from the previous iteration ztmp = self.zc # Do the feedback of Zk from the lower terms first based on the # sequence defined in compSequence if (jj != 0): compSequence = self.parameter["compSequence"] ztmp[int(compSequence[jj - 1]):] = 0 # Add partial feedback of residual estimated wavefront in Zk self.zcomp = self.zcomp + ztmp * feedbackGain # Remove the image distortion if the optical model is not "paraxial" # Only the optical model of "onAxis" or "offAxis" is considered here I1.compensate(inst, self, self.zcomp, model) I2.compensate(inst, self, self.zcomp, model) # Check the image condition. If there is the problem, done with this __singleItr(). if (I1.caustic == True or I2.caustic == True): self.converge[:, jj] = self.converge[:, jj - 1] self.caustic = True return # Correct the defocal images if I1 and I2 are belong to different # sources, which is determined by the (fieldX, field Y) I1, I2 = self.__applyI1I2pMask(I1, I2) # Solve the Poisson's equation self.zc, self.West = self.__solvePoissonEq(inst, I1, I2, jj) # Record/ calculate the Zk coefficient and wavefront if (compMode == "zer"): self.converge[:, jj] = self.zcomp + self.zc self.wcomp = self.West + ZernikeAnnularEval( np.concatenate(([0, 0, 0], self.zcomp[3:])), inst.xoSensor, inst.yoSensor, self.parameter["zobsR"]) else: # Once we run into caustic, stop here, results may be close to real aberration. # Continuation may lead to disatrous results. self.converge[:, jj] = self.converge[:, jj - 1] # Record the coefficients of normal/ annular Zernike polynomials after z4 # in unit of nm self.zer4UpNm = self.converge[3:, jj] * 1e9 # Status of iteration stopItr = False # Calculate the difference if (jj > 0): diffZk = np.sum( np.abs(self.converge[:, jj] - self.converge[:, jj - 1])) * 1e9 # Check the Status of iteration if (diffZk < tol): stopItr = True # Update the current iteration time self.currentItr += 1 # Show the Zk coefficients in interger in each iteration if (self.debugLevel >= 2): tmp = self.zer4UpNm print("itr = %d, z4-z%d" % (jj, self.parameter["numTerms"])) print(np.rint(tmp)) return stopItr
def _genNormalizedFunc(self, r, theta, z, e): func = r * ZernikeAnnularEval(z, r * np.cos(theta), r * np.sin(theta), e) ** 2 return func
def testZernikeAnnularEval(self): surface = ZernikeAnnularEval(self.zerCoef, self.xx, self.yy, self.obscuration) self._checkAnsWithFile(surface, "annularZernikeEval.txt")
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 aperture pixel size apertureDiameter = self._inst.getApertureDiameter() sensorFactor = self._inst.getSensorFactor() dimOfDonut = self._inst.getDimOfDonutOnSensor() aperturePixelSize = apertureDiameter * sensorFactor / dimOfDonut # Calculate the differential Omega dOmega = aperturePixelSize**2 # Solve the Poisson's equation based on the type of algorithm numTerms = self.getNumOfZernikes() zobsR = self.getObsOfZernikes() PoissonSolver = self.getPoissonSolverName() if PoissonSolver == "fft": # Use the differential method by fft to solve the Poisson's # equation # Parameter to determine the threshold of calculating I0. sumclipSequence = self.getSignalClipSequence() cliplevel = sumclipSequence[iOutItr] # Generate the v, u-coordinates on pupil plane padDim = self.getFftDimension() v, u = np.mgrid[-0.5 / aperturePixelSize:0.5 / aperturePixelSize:1.0 / padDim / aperturePixelSize, -0.5 / aperturePixelSize:0.5 / aperturePixelSize:1.0 / padDim / aperturePixelSize, ] # Show the threshold and pupil coordinate information if self.debugLevel >= 3: print("iOuter=%d, cliplevel=%4.2f" % (iOutItr, cliplevel)) print(v.shape) # Calculate the const of fft: # FT{Delta W} = -4*pi^2*(u^2+v^2) * FT{W} u2v2 = -4 * (np.pi**2) * (u * u + v * v) # Set origin to Inf to result in 0 at origin after filtering ctrIdx = int(np.floor(padDim / 2.0)) u2v2[ctrIdx, ctrIdx] = np.inf # Calculate the wavefront signal Sini = self._createSignal(I1, I2, cliplevel) # Find the just-outside and just-inside indices of a ring in pixels # This is for the use in setting dWdn = 0 boundaryT = self.getBoundaryThickness() struct = generate_binary_structure(2, 1) struct = iterate_structure(struct, boundaryT) ApringOut = np.logical_xor( binary_dilation(self.mask_pupil, structure=struct), self.mask_pupil).astype(int) ApringIn = np.logical_xor( binary_erosion(self.mask_pupil, structure=struct), self.mask_pupil).astype(int) bordery, borderx = np.nonzero(ApringOut) # Put the signal in boundary (since there's no existing Sestimate, # S just equals self.S as the initial condition of SCF S = Sini.copy() for jj in range(self.getNumOfInnerItr()): # Calculate FT{S} SFFT = np.fft.fftshift(np.fft.fft2(np.fft.fftshift(S))) # Calculate W by W=IFT{ FT{S}/(-4*pi^2*(u^2+v^2)) } W = np.fft.fftshift( np.fft.irfft2(np.fft.fftshift(SFFT / u2v2), s=S.shape)) # Estimate the wavefront (includes zeroing offset & masking to # the aperture size) # Take the estimated wavefront West = extractArray(W, dimOfDonut) # Calculate the offset offset = West[self.mask_pupil == 1].mean() West = West - offset West[self.mask_pupil == 0] = 0 # Set dWestimate/dn = 0 around boundary WestdWdn0 = West.copy() # Do a 3x3 average around each border pixel, including only # those pixels inside the aperture. This averaging can be # efficiently computed using 1 numpy/scipy vectorized # convolve2d instruction to first sum the values in the 3x3 # region, and dividing by a second convolve2d which counts # the non-zero pixels in each 3x3 region. kernel = np.ones((1 + 2 * boundaryT, 1 + 2 * boundaryT)) tmp = convolve2d(West * ApringIn, kernel, mode="same") tmp /= convolve2d(ApringIn, kernel, mode="same") WestdWdn0[borderx, bordery] = tmp[borderx, bordery] # Take Laplacian to find sensor signal estimate (Delta W = S) del2W = laplace(WestdWdn0) / dOmega # Extend the dimension of signal to the order of 2 for "fft" to # use Sest = padArray(del2W, padDim) # Put signal back inside boundary, leaving the rest of # Sestimate Sest[self.mask_pupil_pad == 1] = Sini[self.mask_pupil_pad == 1] # Need to recheck this condition S = Sest # Calculate the coefficient of normal/ annular Zernike polynomials if self.getCompensatorMode() == "zer": xSensor, ySensor = self._inst.getSensorCoor() zc = ZernikeMaskedFit(West, xSensor, ySensor, numTerms, self.mask_pupil, zobsR) else: zc = np.zeros(numTerms) elif PoissonSolver == "exp": # Use the integration method by serial expansion to solve the # Poisson's equation # Calculate I0 and dI I0, dI = self._getdIandI(I1, I2) # Get the x, y coordinate in mask. The element outside mask is 0. xSensor, ySensor = self._inst.getSensorCoor() xSensor = xSensor * self.mask_comp ySensor = ySensor * self.mask_comp # Create the F matrix and Zernike-related matrixes # Get Zernike and gradient bases from cache. These are each # (nzk, npix, npix) arrays, with the first dimension indicating # the Noll index. zk, dzkdx, dzkdy = self._zernikeBasisCache() # Eqn. (19) from Xin et al., Appl. Opt. 54, 9045-9054 (2015). # F_j = \int (d_z I) Z_j d_Omega F = np.tensordot(dI, zk, axes=((0, 1), (1, 2))) * dOmega # Eqn. (20) from Xin et al., Appl. Opt. 54, 9045-9054 (2015). # M_ij = \int I (grad Z_j) . (grad Z_i) d_Omega # = \int I (dZ_i/dx) (dZ_j/dx) d_Omega # + \int I (dZ_i/dy) (dZ_j/dy) d_Omega Mij = np.einsum("ab,iab,jab->ij", I0, dzkdx, dzkdx) Mij += np.einsum("ab,iab,jab->ij", I0, dzkdy, dzkdy) Mij *= dOmega / (apertureDiameter / 2.0)**2 # Calculate dz focalLength = self._inst.getFocalLength() offset = self._inst.getDefocalDisOffset() dz = 2 * focalLength * (focalLength - offset) / offset # Define zc zc = np.zeros(numTerms) # Consider specific Zk terms only idx = self.getZernikeTerms() # Solve the equation: M*W = F => W = M^(-1)*F zc_tmp = np.linalg.lstsq(Mij[:, idx][idx], F[idx], rcond=None)[0] / dz zc[idx] = zc_tmp # Estimate the wavefront surface based on z4 - z22 # z0 - z3 are set to be 0 instead West = ZernikeAnnularEval(np.concatenate(([0, 0, 0], zc[3:])), xSensor, ySensor, zobsR) return zc, West
def testFunc(self): # Get the path of module modulePath = getModulePath() # Obscuration e = 0 # Calculate the radius dd = np.sqrt(self.xx**2 + self.yy**2) # Define the invalid range idx = (dd > 1) | (dd < e) # Create the Zernike terms Z = np.zeros(22) # Generate the map of Z12 Z[11] = 1 # Calculate the map of Zernike polynomial Zmap = ZernikeAnnularEval(Z, self.xx, self.yy, e) Zmap[idx] = np.nan # Put the elements to be 0 in the invalid region Zmap[np.isnan(Zmap)] = 0 # Check the normalization for Z1 - Z28 e = 0.61 ansValue = np.pi * (1 - e**2) for ii in range(28): Z = np.zeros(28) Z[ii] = 1 funcNor = lambda r, theta: r * ZernikeAnnularEval( Z, r * np.cos(theta), r * np.sin(theta), e)**2 normalization = nquad(funcNor, [[e, 1], [0, 2 * np.pi]])[0] self.assertAlmostEqual(normalization, ansValue) # Check the orthogonality for Z1 - Z28 for jj in range(28): Z1 = np.zeros(28) Z1[jj] = 1 for ii in range(28): if (ii != jj): Z2 = np.zeros(28) Z2[ii] = 1 funcOrtho = lambda r, theta: r*ZernikeAnnularEval(Z1, r*np.cos(theta), r*np.sin(theta), e) * \ ZernikeAnnularEval(Z2, r*np.cos(theta), r*np.sin(theta), e) orthogonality = nquad(funcOrtho, [[e, 1], [0, 2 * np.pi]])[0] self.assertAlmostEqual(orthogonality, 0) # Increase the dimension ZmapInc = padArray(Zmap, Zmap.shape[0] + 20) self.assertAlmostEqual(ZmapInc.shape[0], Zmap.shape[0] + 20) # Decrease the dimension ZmapDec = extractArray(ZmapInc, Zmap.shape[0]) self.assertAlmostEqual(ZmapDec.shape[0], Zmap.shape[0]) # Test the reading of file configFilePath = os.path.join(modulePath, "configData", "cwfs", "instruData", "lsst", "lsst.param") varName = "Focal_length" value = getConfigValue(configFilePath, varName, index=2) self.assertEqual(value, 10.312)