def testExtractArray(self): imgDim = 10 padPixelSize = 20 img, imgPadded = self._padRandomImg(imgDim, padPixelSize) imgExtracted = extractArray(imgPadded, imgDim) self.assertEqual(imgExtracted.shape[0], imgDim)
def testExtractArray(self): imgDim = 10 padPixelSize = 20 img, imgPadded = self._padRandomImg(imgDim, padPixelSize) imgExtracted = extractArray(imgPadded, imgDim) self.assertEqual(imgExtracted.shape[0], imgDim)
def compensate(self, inst, algo, zcCol, model): """Calculate the image compensated from the affection of wavefront. 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 wavefront. model : str Optical model. It can be "paraxial", "onAxis", or "offAxis". Raises ------ RuntimeError input:size zcCol in compensate needs to be a numTerms row column vector. """ # Check the condition of inputs numTerms = algo.getNumOfZernikes() if (zcCol.ndim == 1) and (len(zcCol) != numTerms): raise RuntimeError( "input:size", "zcCol in compensate needs to be a %d row column vector. \n" % numTerms, ) # Dimension of image sm, sn = self.getImg().shape # Dimension of projected image on focal plane projSamples = sm # Let us create a look-up table for x -> xp first. luty, lutx = np.mgrid[-(projSamples / 2 - 0.5):(projSamples / 2 + 0.5), -(projSamples / 2 - 0.5):(projSamples / 2 + 0.5), ] sensorFactor = inst.getSensorFactor() lutx = lutx / (projSamples / 2 / sensorFactor) luty = luty / (projSamples / 2 / sensorFactor) # Set up the mapping lutxp, lutyp, J = self._aperture2image(inst, algo, zcCol, lutx, luty, projSamples, model) show_lutxyp = self._showProjection(lutxp, lutyp, sensorFactor, projSamples, raytrace=False) if np.all(show_lutxyp <= 0): self.caustic = True return # Extend the dimension of image by 20 pixel in x and y direction show_lutxyp = padArray(show_lutxyp, projSamples + 20) # Get the binary matrix of image on pupil plane if raytrace=False struct0 = generate_binary_structure(2, 1) struct = iterate_structure(struct0, 4) struct = binary_dilation(struct, structure=struct0, iterations=2).astype(int) show_lutxyp = binary_dilation(show_lutxyp, structure=struct) show_lutxyp = binary_erosion(show_lutxyp, structure=struct) # Extract the region from the center of image and get the original one show_lutxyp = extractArray(show_lutxyp, projSamples) # Recenter the image imgRecenter = self.centerOnProjection(self.getImg(), show_lutxyp.astype(float), window=20) self.updateImage(imgRecenter) # Construct the interpolant to get the intensity on (x', p') plane # that corresponds to the grid points on (x,y) yp, xp = np.mgrid[-(sm / 2 - 0.5):(sm / 2 + 0.5), -(sm / 2 - 0.5):(sm / 2 + 0.5)] xp = xp / (sm / 2 / sensorFactor) yp = yp / (sm / 2 / sensorFactor) # Put the NaN to be 0 for the interpolate to use lutxp[np.isnan(lutxp)] = 0 lutyp[np.isnan(lutyp)] = 0 # Construct the function for interpolation ip = RectBivariateSpline(yp[:, 0], xp[0, :], self.getImg(), kx=1, ky=1) # Construct the projected image by the interpolation lutIp = ip(lutyp, lutxp, grid=False) # Calculate the image on focal plane with compensation based on flux # conservation # I(x, y)/I'(x', y') = J = (dx'/dx)*(dy'/dy) - (dx'/dy)*(dy'/dx) self.updateImage(lutIp * J) if self.defocalType == DefocalType.Extra: self.updateImage(np.rot90(self.getImg(), k=2)) # Put NaN to be 0 imgCompensate = self.getImg() imgCompensate[np.isnan(imgCompensate)] = 0 # Check the compensated image has the problem or not. # The negative value means the over-compensation from wavefront error if np.any(imgCompensate < 0) and np.all(self.image0 >= 0): print( "WARNING: negative scale parameter, image is within caustic, zcCol (in um)=\n" ) self.caustic = True # Put the overcompensated part to be 0 imgCompensate[imgCompensate < 0] = 0 self.updateImage(imgCompensate)
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 calc_pssn(array, wlum, aType="opd", D=8.36, r0inmRef=0.1382, zen=0, pmask=0, imagedelta=0, fno=1.2335, debugLevel=0): """ Calculate the normalized point source sensitivity (PSSN). Arguments: array {[ndarray]} -- Array that contains either opd or pdf. opd need to be in microns. wlum {[float]} -- Wavelength in microns. Keyword Arguments: aType {str} -- What is used to calculate pssn - either opd or psf. (default: {"opd"}) D {float} -- Side length of OPD image in meter. (default: {8.36}) r0inmRef {float} -- Fidicial atmosphere r0 @ 500nm in meter, Konstantinos uses 0.20. (default: {0.1382}) zen {float} -- Telescope zenith angle in degree. (default: {0}) pmask {float/ ndarray} -- Pupil mask. when opd is used, it can be generated using opd image, we can put 0 or -1 or whatever here. When psf is used, this needs to be provided separately with same size as array. (default: {0}) imagedelta {float} -- Only needed when psf is used. use 0 for opd. (default: {0}) fno {float} -- Only needed when psf is used. use 0 for opd. (default: {1.2335}) debugLevel {int} -- Debug level. The higher value gives more information. (default: {0}) Returns: [float] -- PSSN value. """ # Only needed for psf: pmask, imagedelta, fno # THE INTERNAL RESOLUTION THAT FFTS OPERATE ON IS VERY IMPORTANT # TO THE ACCUARCY OF PSSN. # WHEN TYPE='OPD', NRESO=SIZE(ARRAY,1) # WHEN TYPE='PSF', NRESO=SIZE(PMASK,1) # for the psf option, we can not first convert psf back to opd then # start over, # because psf=|exp(-2*OPD)|^2. information has been lost in the | |^2. # we need to go forward with psf->mtf, # and take care of the coordinates properly. # PSSN = (n_eff)_atm / (n_eff)_atm+sys # (n_eff))_atm = 1 / (int (PSF^2)_atm dOmega) # (n_eff))_atm+sys = 1 / (int (PSF^2)_atm+sys dOmega) # Check the type is "OPD" or "PSF" if aType not in ("opd", "psf"): raise ValueError("The type of %s is not allowed." % aType) # Squeeze the array if necessary if (array.ndim == 3): array2D = array[0, :, :].squeeze() # Get the k value (magnification ratio used in creating MTF) if (aType == "opd"): try: m = max(array2D.shape) except NameError: m = max(array.shape) k = 1 elif (aType == "psf"): m = max(pmask.shape) # Pupil needs to be padded k times larger to get imagedelta # Do not know where to find this formular. Check with Bo. k = fno * wlum / imagedelta # Get the modulation transfer function with the van Karman power spectrum mtfa = createMTFatm(D, m, k, wlum, zen, r0inmRef, model="vonK") # Get the pupil function if (aType == "opd"): try: iad = (array2D != 0) except NameError: iad = (array != 0) elif (aType == "psf"): # Add even number mk = int(m + np.rint((m * (k - 1) + 1e-5) / 2) * 2) # padArray(pmask, m) iad = pmask # OPD --> PSF --> OTF --> OTF' (OTF + atmosphere) --> PSF' # Check with Bo that we could get OTF' or PSF' from PhoSim or not directly. # The above question might not be a concern in the simulation. # However, for the real image, it loooks like this is hard to do # What should be the standard way to judge the PSSN in the real telescope? # OPD is zero for perfect telescope opdt = np.zeros((m, m)) # OPD to PSF psft = opd2psf(opdt, iad, wlum, imagedelta=imagedelta, sensorFactor=1, fno=fno, debugLevel=debugLevel) # PSF to optical transfer function (OTF) otft = psf2otf(psft) # Add atmosphere to perfect telescope otfa = otft * mtfa # OTF to PSF psfa = otf2psf(otfa) # Atmospheric PSS (point spread sensitivity) = 1/neff_atm pssa = np.sum(psfa**2) # Calculate PSF with error (atmosphere + system) if (aType == "opd"): if (array.ndim == 2): ninst = 1 else: ninst = array.shape[0] for ii in range(ninst): if (array.ndim == 2): array2D = array else: array2D = array[ii, :, :].squeeze() psfei = opd2psf(array2D, iad, wlum, debugLevel=debugLevel) if (ii == 0): psfe = psfei else: psfe += psfei # Do the normalization based on the number of instrument psfe = psfe / ninst elif (aType == "psf"): if (array.shape[0] == mk): psfe = array elif (array.shape[0] > mk): psfe = extractArray(array, mk) else: print("calc_pssn: image provided too small, %d < %d x %6.4f." % (array.shape[0], m, k)) print("IQ is over-estimated !!!") psfe = padArray(array, mk) # Do the normalization of PSF psfe = psfe / np.sum(psfe) * np.sum(psft) # OTF with system error otfe = psf2otf(psfe) # Add the atmosphere error # OTF with system and atmosphere errors otftot = otfe * mtfa # PSF with system and atmosphere errors psftot = otf2psf(otftot) # atmospheric + error PSS pss = np.sum(psftot**2) # normalized PSS pssn = pss / pssa if (debugLevel >= 3): print("pssn = %10.8e/%10.8e = %6.4f." % (pss, pssa, pssn)) return pssn
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 compensate(self, inst, algo, zcCol, model): """ Calculate the image compensated from the affection of wavefront. 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 wavefront. model {[string]} -- Optical model. It can be "paraxial", "onAxis", or "offAxis". Raises: Exception -- Number of terms of normal/ annular Zernike polynomilas does not match the needed number for compensation to use. """ # Check the condition of inputs numTerms = algo.parameter["numTerms"] if ((zcCol.ndim == 1) and (len(zcCol) != numTerms)): raise RuntimeError( "input:size", "zcCol in compensate needs to be a %d row column vector. \n" % numTerms) # Dimension of image sm, sn = self.__image.image.shape # Dimenstion of projected image on focal plane projSamples = sm # Let us create a look-up table for x -> xp first. luty, lutx = np.mgrid[-(projSamples / 2 - 0.5):(projSamples / 2 + 0.5), -(projSamples / 2 - 0.5):(projSamples / 2 + 0.5)] sensorFactor = inst.parameter["sensorFactor"] lutx = lutx / (projSamples / 2 / sensorFactor) luty = luty / (projSamples / 2 / sensorFactor) # Set up the mapping lutxp, lutyp, J = self.__aperture2image(inst, algo, zcCol, lutx, luty, projSamples, model) show_lutxyp = self.__showProjection(lutxp, lutyp, sensorFactor, projSamples, raytrace=False) if (np.all(show_lutxyp <= 0)): self.caustic = True return # Calculate the weighting center (x, y) and radius realcx, realcy = self.__image.getCenterAndR_ef()[0:2] # Extend the dimension of image by 20 pixel in x and y direction show_lutxyp = padArray(show_lutxyp, projSamples + 20) # Get the binary matrix of image on pupil plane if raytrace=False struct0 = generate_binary_structure(2, 1) struct = iterate_structure(struct0, 4) struct = binary_dilation(struct, structure=struct0, iterations=2).astype(int) show_lutxyp = binary_dilation(show_lutxyp, structure=struct) show_lutxyp = binary_erosion(show_lutxyp, structure=struct) # Extract the region from the center of image and get the original one show_lutxyp = extractArray(show_lutxyp, projSamples) # Calculate the weighting center (x, y) and radius projcx, projcy = self.__image.getCenterAndR_ef( image=show_lutxyp.astype(float))[0:2] # Shift the image to center of projection on pupil # +(-) means we need to move image to the right (left) shiftx = projcx - realcx # +(-) means we need to move image upward (downward) shifty = projcy - realcy self.__image.image = np.roll(self.__image.image, int(np.round(shifty)), axis=0) self.__image.image = np.roll(self.__image.image, int(np.round(shiftx)), axis=1) # Construct the interpolant to get the intensity on (x', p') plane # that corresponds to the grid points on (x,y) yp, xp = np.mgrid[-(sm / 2 - 0.5):(sm / 2 + 0.5), -(sm / 2 - 0.5):(sm / 2 + 0.5)] xp = xp / (sm / 2 / sensorFactor) yp = yp / (sm / 2 / sensorFactor) # Put the NaN to be 0 for the interpolate to use lutxp[np.isnan(lutxp)] = 0 lutyp[np.isnan(lutyp)] = 0 # Construct the function for interpolation ip = RectBivariateSpline(yp[:, 0], xp[0, :], self.__image.image, kx=1, ky=1) # Construct the projected image by the interpolation lutIp = np.zeros(lutxp.shape[0] * lutxp.shape[1]) for ii, (xx, yy) in enumerate(zip(lutxp.ravel(), lutyp.ravel())): lutIp[ii] = ip(yy, xx) lutIp = lutIp.reshape(lutxp.shape) # Calaculate the image on focal plane with compensation based on flux conservation # I(x, y)/I'(x', y') = J = (dx'/dx)*(dy'/dy) - (dx'/dy)*(dy'/dx) self.__image.image = lutIp * J if (self.atype == "extra"): self.__image.image = np.rot90(self.__image.image, k=2) # Put NaN to be 0 self.__image.image[np.isnan(self.__image.image)] = 0 # Check the compensated image has the problem or not. # The negative value means the over-compensation from wavefront error if (np.any(self.__image.image < 0) and np.all(self.image0 >= 0)): print( "WARNING: negative scale parameter, image is within caustic, zcCol (in um)=\n" ) self.caustic = True # Put the overcompensated part to be 0. self.__image.image[self.__image.image < 0] = 0
def compensate(self, inst, algo, zcCol, model): """Calculate the image compensated from the affection of wavefront. 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 wavefront. model : str Optical model. It can be "paraxial", "onAxis", or "offAxis". Raises ------ RuntimeError input:size zcCol in compensate needs to be a numTerms row column vector. """ # Check the condition of inputs numTerms = algo.getNumOfZernikes() if ((zcCol.ndim == 1) and (len(zcCol) != numTerms)): raise RuntimeError("input:size", "zcCol in compensate needs to be a %d row column vector. \n" % numTerms) # Dimension of image sm, sn = self._image.getImg().shape # Dimenstion of projected image on focal plane projSamples = sm # Let us create a look-up table for x -> xp first. luty, lutx = np.mgrid[-(projSamples/2 - 0.5):(projSamples/2 + 0.5), -(projSamples/2 - 0.5):(projSamples/2 + 0.5)] sensorFactor = inst.getSensorFactor() lutx = lutx/(projSamples/2/sensorFactor) luty = luty/(projSamples/2/sensorFactor) # Set up the mapping lutxp, lutyp, J = self._aperture2image(inst, algo, zcCol, lutx, luty, projSamples, model) show_lutxyp = self._showProjection(lutxp, lutyp, sensorFactor, projSamples, raytrace=False) if (np.all(show_lutxyp <= 0)): self.caustic = True return # Calculate the weighting center (x, y) and radius realcx, realcy = self._image.getCenterAndR_ef()[0:2] # Extend the dimension of image by 20 pixel in x and y direction show_lutxyp = padArray(show_lutxyp, projSamples+20) # Get the binary matrix of image on pupil plane if raytrace=False struct0 = generate_binary_structure(2, 1) struct = iterate_structure(struct0, 4) struct = binary_dilation(struct, structure=struct0, iterations=2).astype(int) show_lutxyp = binary_dilation(show_lutxyp, structure=struct) show_lutxyp = binary_erosion(show_lutxyp, structure=struct) # Extract the region from the center of image and get the original one show_lutxyp = extractArray(show_lutxyp, projSamples) # Calculate the weighting center (x, y) and radius projcx, projcy = self._image.getCenterAndR_ef(image=show_lutxyp.astype(float))[0:2] # Shift the image to center of projection on pupil # +(-) means we need to move image to the right (left) shiftx = projcx - realcx # +(-) means we need to move image upward (downward) shifty = projcy - realcy self._image.updateImage(np.roll(self._image.getImg(), int(np.round(shifty)), axis=0)) self._image.updateImage(np.roll(self._image.getImg(), int(np.round(shiftx)), axis=1)) # Construct the interpolant to get the intensity on (x', p') plane # that corresponds to the grid points on (x,y) yp, xp = np.mgrid[-(sm/2 - 0.5):(sm/2 + 0.5), -(sm/2 - 0.5):(sm/2 + 0.5)] xp = xp/(sm/2/sensorFactor) yp = yp/(sm/2/sensorFactor) # Put the NaN to be 0 for the interpolate to use lutxp[np.isnan(lutxp)] = 0 lutyp[np.isnan(lutyp)] = 0 # Construct the function for interpolation ip = RectBivariateSpline(yp[:, 0], xp[0, :], self._image.getImg(), kx=1, ky=1) # Construct the projected image by the interpolation lutIp = np.zeros(lutxp.shape[0]*lutxp.shape[1]) for ii, (xx, yy) in enumerate(zip(lutxp.ravel(), lutyp.ravel())): lutIp[ii] = ip(yy, xx) lutIp = lutIp.reshape(lutxp.shape) # Calaculate the image on focal plane with compensation based on flux # conservation # I(x, y)/I'(x', y') = J = (dx'/dx)*(dy'/dy) - (dx'/dy)*(dy'/dx) self._image.updateImage(lutIp * J) if (self.defocalType == DefocalType.Extra): self._image.updateImage(np.rot90(self._image.getImg(), k=2)) # Put NaN to be 0 holdedImg = self._image.getImg() holdedImg[np.isnan(holdedImg)] = 0 self._image.updateImage(holdedImg) # Check the compensated image has the problem or not. # The negative value means the over-compensation from wavefront error if (np.any(self._image.getImg() < 0) and np.all(self.image0 >= 0)): print("WARNING: negative scale parameter, image is within caustic, zcCol (in um)=\n") self.caustic = True # Put the overcompensated part to be 0 holdedImg = self._image.getImg() holdedImg[holdedImg < 0] = 0 self._image.updateImage(holdedImg)
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)