def setUp(self): self.holeshape = "hex" # directory containing the test data data_dir = os.path.join(os.path.dirname(__file__), 'test_data/find_affine2d_parameters') self.data_dir = data_dir print(data_dir) if not os.path.exists(data_dir): os.makedirs(data_dir) pixel = 0.0656 * u.arcsec.to(u.rad) npix = 87 wave = np.array([ (1.0, 4.3e-6), ]) # m over = 3 holeshape = 'hex' rotd_true = 9.25 rotdegs = (8.0, 9.0, 10.0, 11.0, 12.0 ) # search in this range of rotation (degrees) # store the real affine to be uased to create test data self.affine = utils.Affine2d( rotradccw=np.pi * utils.avoidhexsingularity(rotd_true) / 180.0, name="{0:.4}".format(float(rotd_true))) self.affine.show(label="Creating affine2d for PSF data") # create image data to find its rotation, as a sample test... imagefn = data_dir + "/imagedata.fits" jw = NRM_Model(mask='jwst', holeshape="hex", affine2d=self.affine) jw.set_pixelscale(pixel) jw.simulate(fov=npix, bandpass=wave, over=over) fits.writeto(imagefn, jw.psf, overwrite=True) imagedata = jw.psf.copy() del jw fits.getdata(imagefn) psf_offset = (0.0, 0.0) print("driver:", rotdegs) mx, my, sx, sy, xo, yo, = (1.0, 1.0, 0.0, 0.0, 0.0, 0.0) aff_best_rot = FAP.find_rotation(imagedata, psf_offset, rotdegs, mx, my, sx, sy, xo, yo, pixel, npix, wave, over, holeshape, outdir=data_dir) print(aff_best_rot) self.aff_best_rot = aff_best_rot
def create_data(imdir, rot, ov): """ imdir: directory for simulated fits image data rot: pupil rotation in degrees ov: oversample for simulation Writes sim data to fitsimdir """ npix = 81 wave = 4.3e-6 # SI fnfmt = '/psf_nrm_{2:.1f}_{0}_{1}_rot{3:.3f}d.fits' # expects strings of %(npix, holeshape, wave/um, rot_d) rot = utils.avoidhexsingularity(rot) # in utils affine_rot = utils.Affine2d(rotradccw=np.pi*rot/180.0, name="rot{0:.3f}d".format(rot)) # in utils jw = NRM_Model(mask='jwst', holeshape='hex', affine2d=affine_rot) jw.set_pixelscale(PIXELSCALE_r) jw.simulate(fov=81, bandpass=MONOF430M, over=ov) psffn = fnfmt.format(npix, 'hex', wave/um, rot) fits.writeto(imdir+psffn, jw.psf, overwrite=True) header = fits.getheader(imdir+psffn) header = utils.affinepars2header(header, affine_rot) fits.update(imdir+psffn, jw.psf, header=header) del jw return psffn # filename only, not full path
def find_rotation( imagedata, rotdegs, mx, my, sx, sy, xo, yo, # for Affine2d pixel, npix, bandpass, over, holeshape, outdir=None): # for nrm_model """ AS AZG 2018 08 Ann Arbor Develop the rotation loop first """ vprint("Before Loop: ", rotdegs) #Extend this name to include multi-paramater searches? psffmt = 'psf_nrm_{0:d}_{1:s}_{2:.3f}um_r{3:.3f}deg.fits' # expect (npix, holeshape, bandpass/um, scl) if hasattr(rotdegs, '__iter__') is False: rotdegs = (rotdegs, ) affine2d_list = create_afflist_rot(rotdegs, mx, my, sx, sy, xo, yo) crosscorr_rots = [] for (rot, aff) in zip(rotdegs, affine2d_list): vprint(aff.name + "...") jw = NRM_Model(mask='jwst', holeshape=holeshape, over=over, affine2d=aff) jw.set_pixelscale(pixel) jw.simulate(fov=npix, bandpass=bandpass, over=over) psffn = psffmt.format(npix, holeshape, bandpass / um, rot) if outdir: fits.PrimaryHDU(data=jw.psf).writeto(outdir + "/" + psffn, overwrite=True) fits.writeto(psffn, jw.psf, overwrite=True) header = fits.getheader(psffn) utils.affinepars2header(header, aff) fits.update(psffn, jw.psf, header=header) crosscorr_rots.append(utils.rcrosscorrelate(imagedata, jw.psf).max()) del jw vprint("Debug: ", crosscorr_rots, rotdegs) rot_measured_d, max_cor = utils.findpeak_1d(crosscorr_rots, rotdegs) vprint("Rotation measured: max correlation {1:.3e}", rot_measured_d, max_cor) # return convenient affine2d return utils.Affine2d(rotradccw=np.pi * rot_measured_d / 180.0, name="{0:.4f}".format(rot_measured_d))
def create_afflist_rot(rotdegs, mx, my, sx, sy, xo, yo): """ create a list of affine objects with various rotations to use in order to go through and find which fits some image plane data best """ alist = [] for nrot, rotd in enumerate(rotdegs): rotd_ = utils.avoidhexsingularity(rotd) alist.append( utils.Affine2d(rotradccw=np.pi * rotd_ / 180.0, name="affrot_{0:+.3f}".format(rotd_))) return alist
def create_afflist_scales(scales, mx, my, sx, sy, xo, yo): """ create a list of affine objects with various uniform pixel scale factors (typically close to unity) to use in order to go through and find which fits some image plane data best if pixelscale in """ alist = [] for nscl, scl in enumerate(scales): alist.append( utils.Affine2d(mx * scl, my * scl, sx * scl, sy * scl, xo * scl, yo * scl, name="scale_{0:.4f}".format(scl))) return alist
def find_scale( imagedata, affine_best, # best current guess at data geometry cf analytical ideal scales, # scales are near-unity pixel, npix, bandpass, over, holeshape, outdir=None): # for nrm_model """ Preserve incoming "pixel" value, put the scale correction into the Affine2d object Is that kosher??? Should we change the pixel scale and leave affine2d the same? Affine2d can also incorporate unequal x and y scales, shears... For now place scale corrections into the Affine2d object Note - placing isotropic scale change into Affine2d is equivalent to changing the effective image distance in the optical train while insisting that the mask physical geometry does not change, and the wavelength is perfectly knowm AS 2018 10 """ affine_best.show("\tfind_scale") vprint("\tBefore Loop: ", scales) #Extend this name to include multi-paramater searches? psffmt = 'psf_nrm_{0:d}_{1:s}_{2:.3f}um_scl{3:.3f}.fits' # expect (npix, holeshape, bandpass/um, scl) if hasattr(scales, '__iter__') is False: scales = (scales, ) affine2d_list = create_afflist_scales(scales, affine_best.mx, affine_best.my, affine_best.sx, affine_best.sy, affine_best.xo, affine_best.yo) crosscorrs = [] for (scl, aff) in zip(scales, affine2d_list): vprint(aff.name + "...") jw = NRM_Model(mask='jwst', holeshape=holeshape, over=over, affine2d=aff) jw.set_pixelscale(pixel) jw.simulate(fov=npix, bandpass=bandpass, over=over) psffn = psffmt.format(npix, holeshape, bandpass[:, 1][0] / um, scl) if outdir: fits.PrimaryHDU(data=jw.psf).writeto(outdir + "/" + psffn, overwrite=True) fits.writeto(psffn, jw.psf, overwrite=True) header = fits.getheader(psffn) utils.affinepars2header(header, aff) fits.update(psffn, jw.psf, header=header) crosscorrs.append(utils.rcrosscorrelate(imagedata, jw.psf).max()) del jw vprint("\tfind_affine2d_parameters: crosscorrelations", crosscorrs) vprint("\tfind_affine2d_parameters: scales", scales) scl_measured, max_cor = utils.findpeak_1d(crosscorrs, scales) vprint( "\tfind_affine2d_parameters factor measured {0:.5f} Max correlation {1:.3e}" .format(scl_measured, max_cor)) vprint("\tfind_affine2d_parameters pitch from header {0:.3f} mas".format( pixel * rad2mas)) vprint( "\tfind_affine2d_parameters pitch {0:.3f} mas (implemented using affine2d)" .format(scl_measured * pixel * rad2mas)) # return convenient affine2d return utils.Affine2d(affine_best.mx * scl_measured, affine_best.my * scl_measured, affine_best.sx * scl_measured, affine_best.sy * scl_measured, affine_best.xo * scl_measured, affine_best.yo * scl_measured, name="scale_{0:.4f}".format(scl))
def setUp(self): # setup parameters for simulation verbose = 1 overwrite = 1 monochromatic_wavelength_m = np.array([ (1.0, 4.3e-6), ]) mask = 'MASK_NRM' filter = 'F430M' pixel = 0.0656 # arcsec filter_name = 'Monochromatic ' + np.str(monochromatic_wavelength_m) self.filter = filter self.filter_name = filter_name self.monochromatic_wavelength_m = monochromatic_wavelength_m # directory containing the test data datadir = os.path.join(os.path.dirname(__file__), 'test_data/find_pass_affine') if not os.path.exists(datadir): os.makedirs(datadir) self.datadir = datadir # file containing the test data imagefn = datadir + "/simulated_image.fits" # use millidegrees and integers imagefn = imagefn.replace( ".fits", "_truerotmd{0:+05d}.fits".format(int(1000.0 * float(rotd_true)))) self.imagefn = imagefn # savedir for reduced quantities self.savedir = imagefn.replace( ".fits", "_truerotmd{0:+05d}.fits".format(int(1000.0 * float(rotd_true)))) self.savedir = self.datadir + '/' print('\n') print(' >> >> >> >> set-up self.imagefn %s' % self.imagefn) print(' >> >> >> >> set-up self.datadir %s' % self.datadir) print(' >> >> >> >> set-up self.savedir %s' % self.savedir) # Create test data on disk print("avoidhexsingularity: ", utils.avoidhexsingularity(rotd_true)) affine = utils.Affine2d(rotradccw=np.pi * utils.avoidhexsingularity(rotd_true) / 180.0, name="{0:.3}".format(float(rotd_true))) affine.show(label="Creating PSF data with Affine2d") # from nrm_analysis.fringefitting.LG_Model import NRM_Model jw = NRM_Model(mask='jwst', holeshape="hex", affine2d=affine) jw.set_pixelscale(pixel * arcsec2rad) jw.simulate(fov=n_image, bandpass=monochromatic_wavelength_m, over=oversample) # PSF without oversampling fits.writeto(imagefn, jw.psf, overwrite=True) header = fits.getheader(imagefn) header['PIXELSCL'] = (pixel, "arcseconds") header['FILTER'] = filter_name header['PUPIL'] = mask header = utils.affinepars2header(header, affine) fits.update(imagefn, jw.psf, header=header) self.simulated_image = jw.psf.copy() self.affine = affine # Affine2d parameters used to create test simulated_image ("true" value) del jw del affine
print(" simulate_data: ") jw = NRM_Model(mask='jwst', holeshape="hex") jw.simulate(fov=fov, bandpass=bandpass, over=oversample, psf_offset=psf_offset_det) fits.PrimaryHDU(data=jw.psf).writeto(fitsimdir + "all_effects_data.fits", overwrite=True) #**********Convert simulated data to mirage format.******* utils.amisim2mirage(fitsimdir, ("all_effects_data", ), mirexample, filt) if __name__ == "__main__": identity = utils.Affine2d(rotradccw=utils.avoidhexsingularity(0.0), name="affrot_{0:+.3f}deg".format(0.0)) no_pistons = np.zeros((7, )) * 1.0 _psf_offset_det = (0.48, 0.0) no_psf_offset = (0.0, 0.0) rot = 2.0 rot = utils.avoidhexsingularity(rot) aff = utils.Affine2d(rotradccw=np.pi * rot / 180.0, name="affrot_{0:+.3f}d".format(rot)) _rotsearch_d = np.arange(-3, 3.1, 1) #std dev 1, 7 holes, diffraction-limited @ 2um we're at 4um _pistons_w = 0.5 * np.random.normal(0, 1.0, 7) / 14.0 simulate_data(affine2d=aff, psf_offset_det=_psf_offset_det,
def __init__(self, mask=None, v3_yang=0.0, holeshape="circ", pixscale=None, over = 1, log=_default_log, pixweight=None, datapath="", phi=None, refdir="", chooseholes=False, affine2d = None, **kwargs): """ mask will either be a string keyword for built-in values or an NRM_mask_geometry object. pixscale should be input in radians. phi (rad) default changedfrom "perfect" to None (with bkwd compat.) """ if "debug" in kwargs: self.debug=kwargs["debug"] else: self.debug=False # define a handler to write log messages to stdout sh = logging.StreamHandler(stream=sys.stdout) # define the format for the log messages, here: "level name: message" formatter = logging.Formatter("[%(levelname)s]: %(message)s") sh.setFormatter(formatter) self.logger = log self.logger.addHandler(sh) self.holeshape = holeshape self.pixel = pixscale # det pix in rad (square) self.over = over self.maskname = mask # should change to "mask", and mask.maskname is then eg jwst_g7s6c or whatever 2021 feb anand # Cos incoming 'mask' is str, this is a mask object. #elf.mask = mask # should change to "mask", and mask.maskname is then eg jwst_g7s6c or whatever 2021 feb anand self.pixweight = pixweight mask = mask_definitions.NRM_mask_definitions(maskname="jwst_g7s6c", chooseholes=chooseholes, holeshape="hex") self.ctrs = mask.ctrs self.d = mask.hdia self.D = mask.activeD self.N = len(self.ctrs) self.datapath = datapath self.refdir = refdir self.fmt = "%10.4e" if phi: # meters of OPD at central wavelength if phi == "perfect": self.phi = np.zeros(self.N) # backwards compatibility print('LG_Model.__init__(): phi="perfect" deprecated in LG++. Omit phi or use phi=None') else: self.phi = phi else: self.phi = np.zeros(self.N) self.chooseholes = chooseholes """ affine2d property not to be changed in NRM_Model - create a new instance instead Save affine deformation of pupil object or create a no-deformation object. We apply this when sampling the PSF, not to the pupil geometry. """ if affine2d is None: self.affine2d = utils.Affine2d(mx=1.0,my=1.0, sx=0.0,sy=0.0, xo=0.0,yo=0.0, name="Ideal") else: self.affine2d = affine2d
def __init__(self, filt, objname="obj", src='A0V', chooseholes=None, affine2d=None, bandpass=None, nbadpix=4, usebp=True, firstfew=None, nspecbin=None, **kwargs): """ Initialize NIRISS class ARGUMENTS: kwargs: UTR Or just look at the file structure Either user has webbpsf and filter file can be read, or... chooseholes: None, or e.g. ['B2', 'B4', 'B5', 'B6'] for a four-hole mask filt: Filter name string like "F480M" bandpass: None or [(wt,wlen),(wt,wlen),...]. Monochromatic would be e.g. [(1.0, 4.3e-6)] Explicit bandpass arg will replace *all* niriss filter-specific variables with the given bandpass (src, nspecbin, filt), so you can simulate 21cm psfs through something called "F430M". Can also be synphot.spectrum.SourceSpectrum object. firstfew: None or the number of slices to truncate input cube to in memory, the latter for fast developmpent nbadpix: Number of good pixels to use when fixing bad pixels DEPRECATED usebp: Convert to usedq during initialization Internally this is changed to sellf.usedq = usebp immediately for code clarity True (default) do not use DQ with DO_NOT_USE flag in input MAST data when fitting data with model. False: Assume no bad pixels in input noise: standard deviation of noise added to perfect images to enable candid plots without crashing on np.inf limits! Image assumed to be in (np.float64) dn. Suggested noise: 1e-6. src: source spectral type string e.g. "A0V" OR user-defined synphot.spectrum.SourceSpectrum object nspecbin: Number of wavelength bins to use across the bandpass. Replaces deprecated `usespecbin` which set **number of wavelengths to into each bin**, not nbins. """ self.verbose = False if "verbose" in kwargs: self.verbose = kwargs["verbose"] self.noise = None if "noise" in kwargs: self.noise = kwargs["noise"] if "usespecbin" in kwargs: # compatability with previous arg # but not really, usespecbin was binning factor, not number of bins nspecbin = kwargs["usespecbin"] # change how many wavelength bins will be used across the bandpass if nspecbin is None: nspecbin = 19 self.lam_bin = nspecbin # src can be either a spectral type string or a user-defined synphot spectrum object if isinstance(src, synphot.spectrum.SourceSpectrum): print("Using user-defined synphot SourceSpectrum") if chooseholes: print("InstrumentData.NIRISS: ", chooseholes) self.chooseholes = chooseholes # USEBP is USEDQ in the rest of code - use self.usedq = usebp print( "InstrumentData.NIRISS: avoid fitting DO_NOT_USE bad pixels flagged in DQ extension", self.usedq) self.jwst_dqflags() # creates dicts self.bpval, self.bpgroup # self.bpexist set True/False if DQ fits image extension exists/doesn't self.firstfew = firstfew if firstfew is not None: print( "InstrumentData.NIRISS: analysing firstfew={:d} slices".format( firstfew)) self.objname = objname self.filt = filt if bandpass is not None: print( "InstrumentData.NIRISS: OVERRIDING BANDPASS WITH USER-SUPPLIED VALUES." ) print("\t src, filt, nspecbin parameters will not be used") # check type of bandpass. can be synphot spectrum # if so, get throughput and wavelength arrays if isinstance(bandpass, synphot.spectrum.SpectralElement): wl, wt = bandpass._get_arrays(bandpass.waveset) self.throughput = np.array((wt, wl)).T else: self.throughput = np.array(bandpass) # type simplification else: filt_spec = utils.get_filt_spec(self.filt) src_spec = utils.get_src_spec(src) # **NOTE**: As of WebbPSF version 1.0.0 filter is trimmed to where throughput is 10% of peak # For consistency with WebbPSF simultions, use trim=0.1 self.throughput = utils.combine_src_filt(filt_spec, src_spec, trim=0.01, nlambda=nspecbin, verbose=self.verbose, plot=False) self.lam_c, self.lam_w = utils.get_cw_beta(self.throughput) if self.verbose: print("InstrumentData.NIRISS: ", self.filt, ": central wavelength {:.4e} microns, ".format(self.lam_c / um), end="") if self.verbose: print("InstrumentData.NIRISS: ", "fractional bandpass {:.3f}".format(self.lam_w)) self.wls = [ self.throughput, ] if self.verbose: print("self.throughput:\n", self.throughput) # Wavelength info for NIRISS bands F277W, F380M, F430M, or F480M self.wavextension = ([ self.lam_c, ], [ self.lam_w, ]) self.nwav = 1 # these are 'slices' if the data is pure imaging integrations - # nwav is old nomenclature from GPI IFU data. Refactor one day... ############################# # only one NRM on JWST: self.telname = "JWST" self.instrument = "NIRISS" self.arrname = "jwst_g7s6c" # implaneia mask set with this - unify to short form later self.holeshape = "hex" self.mask = NRM_mask_definitions(maskname=self.arrname, chooseholes=chooseholes, holeshape=self.holeshape) # save affine deformation of pupil object or create a no-deformation object. # We apply this when sampling the PSF, not to the pupil geometry. # This will set a default Ideal or a measured rotation, for example, # and include pixel scale changes due to pupil distortion. # Separating detector tilt pixel scale effects from pupil distortion effects is # yet to be determined... see comments in Affine class definition. # AS AZG 2018 08 15 Ann Arbor if affine2d is None: self.affine2d = utils.Affine2d(mx=1.0, my=1.0, sx=0.0, sy=0.0, xo=0.0, yo=0.0, name="Ideal") else: self.affine2d = affine2d # finding centroid from phase slope only considered cv_phase data # when cv_abs data exceeds this cvsupport_threshold. # Absolute value of cv data normalized to unity maximum # for the threshold application. # Data reduction gurus: tweak the threshold value with experience... # Gurus: tweak cvsupport with use... self.cvsupport_threshold = { "F277W": 0.02, "F380M": 0.02, "F430M": 0.02, "F480M": 0.02 } if self.verbose: show_cvsupport_threshold(self) self.threshold = self.cvsupport_threshold[filt]
def __init__(self, mask=None, holeshape="circ", pixscale=None, over=1, log=_default_log, pixweight=None, datapath="", phi=None, refdir="", chooseholes=False, affine2d=None, **kwargs): """ mask will either be a string keyword for built-in values or an NRM_mask_geometry object. pixscale should be input in radians. phi (rad) default changedfrom "perfect" to None (with bkwd compat.) """ # define a handler to write log messages to stdout sh = logging.StreamHandler(stream=sys.stdout) # define the format for the log messages, here: "level name: message" formatter = logging.Formatter("[%(levelname)s]: %(message)s") sh.setFormatter(formatter) self.logger = log self.logger.addHandler(sh) self.holeshape = holeshape self.pixel = pixscale # det pix in rad (square) self.over = over self.maskname = mask self.pixweight = pixweight # WARNING! JWST CHOOSEHOLES CODE NOW DUPLICATED IN mask_definitions.py WARNING! ### holedict = { } # as_built names, C2 open, C5 closed, but as designed coordinates # Assemble holes by actual open segment names. Either the full mask or the # subset-of-holes mask will be V2-reversed after the as_designed ceenters # are installed in the object. allholes = ('b4', 'c2', 'b5', 'b2', 'c1', 'b6', 'c6') b4, c2, b5, b2, c1, b6, c6 = ('b4', 'c2', 'b5', 'b2', 'c1', 'b6', 'c6') holedict['b4'] = [0.00000000, -2.640000] #B4 -> B4 holedict['c2'] = [-2.2863100, 0.0000000] #C5 -> C2 holedict['b5'] = [2.2863100, -1.3200001] #B3 -> B5 holedict['b2'] = [-2.2863100, 1.3200001] #B6 -> B2 holedict['c1'] = [-1.1431500, 1.9800000] #C6 -> C1 holedict['b6'] = [2.2863100, 1.3200001] #B2 -> B6 holedict['c6'] = [1.1431500, 1.9800000] #C1 -> C6 if mask == 'jwst': # as designed MB coordinates (Mathilde Beaulieu, Peter, Anand). # as designed: segments C5 open, C2 closed, meters V2V3 per Paul Lightsey def # as built C5 closed, C2 open # # undistorted pupil coords on PM. These numbers are considered immutable. # as designed seg -> as built seg in comments each ctr entry (no distortion) if chooseholes is not False: #holes B4 B5 C6 asbuilt for orientation testing holelist = [] for h in allholes: if h in chooseholes: holelist.append(holedict[h]) self.ctrs_asdesigned = np.array(holelist) else: # the REAL THING - as_designed 7 hole, m in PM space, no distortion shape (7,2) self.ctrs_asdesigned = np.array([ [0.00000000, -2.640000], #B4 -> B4 as-designed -> as-built mapping [-2.2863100, 0.0000000], #C5 -> C2 [2.2863100, -1.3200001], #B3 -> B5 [-2.2863100, 1.3200001], #B6 -> B2 [-1.1431500, 1.9800000], #C6 -> C1 [2.2863100, 1.3200001], #B2 -> B6 [1.1431500, 1.9800000] ]) #C1 -> C6 self.d = 0.80 * m self.D = 6.5 * m else: try: #print(mask.ctrs) pass except AttributeError: raise AttributeError( "Mask must be either 'jwst' or NRM_mask_geometry object") self.ctrs, self.d, self.D = np.array( mask.ctrs), mask.hdia, mask.activeD if mask == 'jwst': """ Preserve ctrs.as_designed (treat as immutable) Reverse V2 axis coordinates to close C5 open C2, and others follow suit... Preserve ctrs.as_built (treat as immutable) """ self.ctrs_asbuilt = self.ctrs_asdesigned.copy() # create 'live' hole centers in an ideal, orthogonal undistorted xy pupil space, # eg maps open hole C5 in as_designed to C2 as_built, eg C4 unaffacted.... self.ctrs_asbuilt[:, 0] *= -1 # LG++ rotate hole centers by 90 deg to match MAST o/p DMS PSF with # no affine2d transformations 8/2018 AS # LG++ The above aligns the hole patern with the hex analytic FT, # flat top & bottom as seen in DMS data. 8/2018 AS self.ctrs_asbuilt = utils.rotate2dccw(self.ctrs_asbuilt, np.pi / 2.0) # overwrites attributes # create 'live' hole centers in an ideal, orthogonal undistorted xy pupil space, self.ctrs = self.ctrs_asbuilt.copy() self.N = len(self.ctrs) self.datapath = datapath self.refdir = refdir self.fmt = "%10.4e" if phi: # meters of OPD at central wavelength if phi == "perfect": self.phi = np.zeros(self.N) # backwards compatibility vprint( 'LG_Model.__init__(): phi="perfect" deprecated in LG++. Omit phi or use phi=None' ) else: self.phi = phi else: self.phi = np.zeros(self.N) self.chooseholes = chooseholes """ affine2d property not to be changed in NRM_Model - create a new instance instead Save affine deformation of pupil object or create a no-deformation object. We apply this when sampling the PSF, not to the pupil geometry. """ if affine2d is None: self.affine2d = utils.Affine2d(mx=1.0, my=1.0, sx=0.0, sy=0.0, xo=0.0, yo=0.0, name="Ideal") else: self.affine2d = affine2d