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
Beispiel #2
0
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))
Beispiel #4
0
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
Beispiel #5
0
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
Beispiel #6
0
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
Beispiel #8
0
    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,
Beispiel #9
0
    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
Beispiel #10
0
    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]
Beispiel #11
0
    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