From 2bff4fe6017f0e247bfcbfa451121e187ad160a5 Mon Sep 17 00:00:00 2001 From: Rachel Cooper Date: Tue, 5 Nov 2024 16:57:38 -0500 Subject: [PATCH 01/23] f2f 0.8 m, print rotation search to log --- jwst/ami/ami_analyze.py | 13 +++++++++---- jwst/ami/mask_definitions.py | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/jwst/ami/ami_analyze.py b/jwst/ami/ami_analyze.py index 631c780004..45379d594a 100644 --- a/jwst/ami/ami_analyze.py +++ b/jwst/ami/ami_analyze.py @@ -126,15 +126,17 @@ def apply_LG_plus( # Throughput (combined filter and source spectrum) calculated here bandpass = utils.handle_bandpass(bandpass, throughput_model) - rotsearch_d = np.append( + + + if affine2d is None: + rotsearch_d = np.append( np.arange( rotsearch_parameters[0], rotsearch_parameters[1], rotsearch_parameters[2] ), rotsearch_parameters[1], - ) + ) - log.info(f"Initial values to use for rotation search: {rotsearch_d}") - if affine2d is None: + log.info(f"Initial values to use for rotation search: {rotsearch_d}") # affine2d object, can be overridden by user input affine. # do rotation search on uncropped median image (assuming rotation constant over exposure) # replace remaining NaNs in median image with median of surrounding 8 (non-NaN) pixels @@ -173,6 +175,9 @@ def apply_LG_plus( oversample, holeshape, ) + log.info(f'Found rotation: {affine2d.rotradccw:.4f} rad ({np.rad2deg(affine2d.rotradccw):.4f} deg)') + # the affine2d returned here has only rotation... + # to use rotation and scaling/shear, do some matrix multiplication here?? niriss = instrument_data.NIRISS(filt, nrm_model, diff --git a/jwst/ami/mask_definitions.py b/jwst/ami/mask_definitions.py index edf6936531..d761f90c34 100755 --- a/jwst/ami/mask_definitions.py +++ b/jwst/ami/mask_definitions.py @@ -175,5 +175,5 @@ def jwst_g7s6c(chooseholes=None): """ - f2f = 0.82 * m # m flat to flat + f2f = 0.8 * m # m flat to flat return f2f, jwst_g7s6_centers_asbuilt(chooseholes=chooseholes) From f1a2bd9969594bb0cfbe1bcd72aebc0791800cfc Mon Sep 17 00:00:00 2001 From: Rachel Cooper Date: Mon, 11 Nov 2024 10:27:02 -0500 Subject: [PATCH 02/23] option to use affine from commissioning by passing affine2d='commissioning'. Not yet default behavior. --- jwst/ami/ami_analyze_step.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/jwst/ami/ami_analyze_step.py b/jwst/ami/ami_analyze_step.py index 3330e6aa25..2284ef9902 100755 --- a/jwst/ami/ami_analyze_step.py +++ b/jwst/ami/ami_analyze_step.py @@ -182,9 +182,17 @@ def process(self, input): bandpass = self.override_bandpass() if affine2d is not None: - # if it is None, handled in apply_LG_plus - affine2d = self.override_affine2d() - + if affine2d == 'commissioning': + affine2d = utils.Affine2d(mx= 9.92820e-01, + my=9.98540e-01, + sx=6.18605e-03, + sy=-7.27008e-03, + xo=0, + yo=0, + name='commisioning') + else: + affine2d = self.override_affine2d() + # and if it is None, rotation search done in apply_LG_plus # Get the name of the NRM reference file to use nrm_reffile = self.get_reference_file(input_model, 'nrm') From 2027c36f71ff86657c10f7bf4507e2f7bf33d59d Mon Sep 17 00:00:00 2001 From: Rachel Cooper Date: Mon, 18 Nov 2024 11:26:06 -0500 Subject: [PATCH 03/23] Mask definitions read in from NRM reference file in place of hardcoded values in mask_definitions.py. Removed references to other NRMs. --- jwst/ami/mask_definition_ami.py | 130 ++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 jwst/ami/mask_definition_ami.py diff --git a/jwst/ami/mask_definition_ami.py b/jwst/ami/mask_definition_ami.py new file mode 100644 index 0000000000..d4986537ae --- /dev/null +++ b/jwst/ami/mask_definition_ami.py @@ -0,0 +1,130 @@ +# +# Module for defining mask geometry in pupil space +# Replace old mask_definitions by reading info in nrm_model into a similar object. +# + +import numpy as np +import math + +from .utils import rotate2dccw + +m = 1.0 +mm = 1.0e-3 * m +um = 1.0e-6 * m + + +class NRM_definitions(): + + def __init__(self, nrm_model, maskname='jwst_ami', rotdeg=None, chooseholes=None): + """ + Set attributes of NRM_mask_definitions class. + + Parameters + ---------- + nrm_model: NRMModel + datamodel containing NRM reference file data + + rotdeg: list of floats + range of rotations to search (degrees) + + chooseholes: list + None, or e.g. ['B2', 'B4', 'B5', 'B6'] for a four-hole mask + + Returns + ------- + None + """ + if maskname not in ['jwst_ami','jwst_g7s6c']: + raise ValueError("Mask name not supported") + + self.maskname = maskname # there's only one mask but this is used in oifits + + self.read_nrm_model(nrm_model, chooseholes=chooseholes) + + if rotdeg is not None: + self.rotdeg = rotdeg + + def read_nrm_model(self, nrm_model, chooseholes=None): + """ + + Calculate hole centers with appropriate rotation, + set these and other mask geometry details from NRMModel as + attributes. + + Parameters + ---------- + nrm_model: NRMModel + datamodel containing NRM reference file data + chooseholes: list + None, or e.g. ['B2', 'B4', 'B5', 'B6'] for a four-hole mask + + Returns + ------- + f2f: float + flat-to-flat distance of mask holes + ctrs_asbuilt: array + Actual hole centers [meters] + """ + + ctrs_asdesigned = [[nrm_model.x_a1, nrm_model.y_a1], # B4 -> B4 + [nrm_model.x_a2, nrm_model.y_a2], # C5 -> C2 + [nrm_model.x_a3, nrm_model.y_a3], # B3 -> B5 + [nrm_model.x_a4, nrm_model.y_a4], # B6 -> B2 + [nrm_model.x_a5, nrm_model.y_a5], # C6 -> C1 + [nrm_model.x_a6, nrm_model.y_a6], # B2 -> B6 + [nrm_model.x_a7, nrm_model.y_a7]] # C1 -> C6 + + self.hdia = nrm_model.flat_to_flat + self.activeD = nrm_model.diameter + self.OD = nrm_model.pupil_circumscribed + + holedict = {} # as_built names, C2 open, C5 closed, but as designed coordinates + # Assemble holes by actual open segment names (as_built). Either the full mask or the + # subset-of-holes mask will be V2-reversed after the as_designed centers are defined + # Debug orientations with b4,c6,[c2] + allholes = ('b4', 'c2', 'b5', 'b2', 'c1', 'b6', 'c6') + + for hole, coords in zip(allholes,ctrs_asdesigned): + holedict[hole] = coords + + if chooseholes: # holes B4 B5 C6 asbuilt for orientation testing + holelist = [] + for h in allholes: + if h in chooseholes: + holelist.append(holedict[h]) + ctrs_asdesigned = np.array(holelist) + + ctrs_asbuilt = 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 unaffected.... + 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 pattern with the hex analytic FT, + # flat top & bottom as seen in DMS data. 8/2018 AS + ctrs_asbuilt = rotate2dccw(ctrs_asbuilt, np.pi / 2.0) # overwrites attributes + + # create 'live' hole centers in an ideal, orthogonal undistorted xy pupil space, + self.ctrs = ctrs_asbuilt + + def showmask(self): + """ + Calculate the diameter of the smallest centered circle (D) + enclosing the live mask area + + Parameters + ---------- + + Returns + ------- + Diameter of the smallest centered circle + + """ + radii = [] + for ctr in self.ctrs: + radii.append(math.sqrt(ctr[0] * ctr[0] + ctr[1] * ctr[1])) + + return 2.0 * (max(radii) + 0.5 * self.hdia) + From fff610400ba11d45ed822b2f6555bfec554c904e Mon Sep 17 00:00:00 2001 From: Rachel Cooper Date: Mon, 18 Nov 2024 15:34:54 -0500 Subject: [PATCH 04/23] use updated mask_definition_ami --- jwst/ami/instrument_data.py | 11 +++++------ jwst/ami/lg_model.py | 23 ++++++++++------------- jwst/ami/mask_definition_ami.py | 4 ++-- 3 files changed, 17 insertions(+), 21 deletions(-) diff --git a/jwst/ami/instrument_data.py b/jwst/ami/instrument_data.py index 26d4f73d08..d5ef938cfd 100755 --- a/jwst/ami/instrument_data.py +++ b/jwst/ami/instrument_data.py @@ -6,7 +6,7 @@ import logging import numpy as np -from .mask_definitions import NRM_mask_definitions +from .mask_definition_ami import NRM_definition from . import utils from . import bp_fix from stdatamodels.jwst.datamodels import dqflags @@ -91,12 +91,11 @@ def __init__(self, # only one NRM on JWST: self.telname = "JWST" self.instrument = "NIRISS" - self.arrname = "jwst_g7s6c" - self.holeshape = "hex" - self.mask = NRM_mask_definitions( + self.arrname = "jwst_ami" + self.mask = NRM_definition( + nrm_model, maskname=self.arrname, - chooseholes=self.chooseholes, - holeshape=self.holeshape, + chooseholes=self.chooseholes ) # save affine deformation of pupil object or create a no-deformation object. diff --git a/jwst/ami/lg_model.py b/jwst/ami/lg_model.py index 2981baaa8a..44d94a6194 100755 --- a/jwst/ami/lg_model.py +++ b/jwst/ami/lg_model.py @@ -6,7 +6,7 @@ from . import leastsqnrm as leastsqnrm from . import analyticnrm2 from . import utils -from . import mask_definitions +from . import mask_definition_ami log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) @@ -22,7 +22,7 @@ class NrmModel: """ A class for conveniently dealing with an "NRM object" This should be able - to take an NRM_mask_definitions object for mask geometry. + to take an NRM_definition object for mask geometry. Defines mask geometry and detector-scale parameters. Simulates PSF (broadband or monochromatic) Builds a fringe model - either by user definition, or automated to data @@ -35,7 +35,7 @@ class NrmModel: def __init__( self, mask=None, - holeshape="circ", + holeshape="hex", pixscale=None, over=1, pixweight=None, @@ -91,19 +91,16 @@ def __init__( self.over = over self.pixweight = pixweight - # mask = "jwst" - # self.maskname = mask - - # get these from mask_definitions instead + # get these from mask_definition_ami instead if mask is None: - log.info("No mask name specified for model, using jwst_g7s6c") - mask = mask_definitions.NRM_mask_definitions( - maskname="jwst_g7s6c", chooseholes=chooseholes, holeshape="hex" + log.info("Using JWST AMI mask geometry from NrmModel") + mask = mask_definition_ami.NRM_definition( + maskname="jwst_ami", chooseholes=chooseholes ) elif isinstance(mask, str): - mask = mask_definitions.NRM_mask_definitions( - maskname=mask, chooseholes=chooseholes, holeshape="hex" - ) + mask = mask_definition_ami.NRM_definition( + maskname=mask, chooseholes=chooseholes + ) # retain ability to possibly use other named masks, for now self.ctrs = mask.ctrs self.d = mask.hdia self.D = mask.activeD diff --git a/jwst/ami/mask_definition_ami.py b/jwst/ami/mask_definition_ami.py index d4986537ae..3a4b8c14fb 100644 --- a/jwst/ami/mask_definition_ami.py +++ b/jwst/ami/mask_definition_ami.py @@ -13,11 +13,11 @@ um = 1.0e-6 * m -class NRM_definitions(): +class NRM_definition(): def __init__(self, nrm_model, maskname='jwst_ami', rotdeg=None, chooseholes=None): """ - Set attributes of NRM_mask_definitions class. + Set attributes of NRM_definition class. Parameters ---------- From 5a3fd3e6bcc0098406b940abe2150411a1f3edb0 Mon Sep 17 00:00:00 2001 From: Rachel Cooper Date: Mon, 18 Nov 2024 16:02:37 -0500 Subject: [PATCH 05/23] Keep holeshape for model, converted spaces to tabs --- jwst/ami/instrument_data.py | 1 + jwst/ami/mask_definition_ami.py | 192 ++++++++++++++++---------------- 2 files changed, 97 insertions(+), 96 deletions(-) diff --git a/jwst/ami/instrument_data.py b/jwst/ami/instrument_data.py index d5ef938cfd..08d30bdc6e 100755 --- a/jwst/ami/instrument_data.py +++ b/jwst/ami/instrument_data.py @@ -92,6 +92,7 @@ def __init__(self, self.telname = "JWST" self.instrument = "NIRISS" self.arrname = "jwst_ami" + self.holeshape = 'hex' self.mask = NRM_definition( nrm_model, maskname=self.arrname, diff --git a/jwst/ami/mask_definition_ami.py b/jwst/ami/mask_definition_ami.py index 3a4b8c14fb..04cfe730f7 100644 --- a/jwst/ami/mask_definition_ami.py +++ b/jwst/ami/mask_definition_ami.py @@ -15,116 +15,116 @@ class NRM_definition(): - def __init__(self, nrm_model, maskname='jwst_ami', rotdeg=None, chooseholes=None): - """ - Set attributes of NRM_definition class. - - Parameters - ---------- - nrm_model: NRMModel - datamodel containing NRM reference file data - - rotdeg: list of floats - range of rotations to search (degrees) - - chooseholes: list - None, or e.g. ['B2', 'B4', 'B5', 'B6'] for a four-hole mask - - Returns - ------- - None - """ - if maskname not in ['jwst_ami','jwst_g7s6c']: - raise ValueError("Mask name not supported") - - self.maskname = maskname # there's only one mask but this is used in oifits - - self.read_nrm_model(nrm_model, chooseholes=chooseholes) - - if rotdeg is not None: - self.rotdeg = rotdeg - - def read_nrm_model(self, nrm_model, chooseholes=None): - """ - - Calculate hole centers with appropriate rotation, - set these and other mask geometry details from NRMModel as - attributes. - - Parameters - ---------- - nrm_model: NRMModel - datamodel containing NRM reference file data - chooseholes: list - None, or e.g. ['B2', 'B4', 'B5', 'B6'] for a four-hole mask - - Returns - ------- - f2f: float - flat-to-flat distance of mask holes - ctrs_asbuilt: array - Actual hole centers [meters] - """ - - ctrs_asdesigned = [[nrm_model.x_a1, nrm_model.y_a1], # B4 -> B4 - [nrm_model.x_a2, nrm_model.y_a2], # C5 -> C2 - [nrm_model.x_a3, nrm_model.y_a3], # B3 -> B5 - [nrm_model.x_a4, nrm_model.y_a4], # B6 -> B2 - [nrm_model.x_a5, nrm_model.y_a5], # C6 -> C1 - [nrm_model.x_a6, nrm_model.y_a6], # B2 -> B6 - [nrm_model.x_a7, nrm_model.y_a7]] # C1 -> C6 + def __init__(self, nrm_model, maskname='jwst_ami', rotdeg=None, chooseholes=None): + """ + Set attributes of NRM_definition class. + + Parameters + ---------- + nrm_model: NRMModel + datamodel containing NRM reference file data + + rotdeg: list of floats + range of rotations to search (degrees) + + chooseholes: list + None, or e.g. ['B2', 'B4', 'B5', 'B6'] for a four-hole mask + + Returns + ------- + None + """ + if maskname not in ['jwst_ami','jwst_g7s6c']: + raise ValueError("Mask name not supported") + + self.maskname = maskname # there's only one mask but this is used in oifits + + self.read_nrm_model(nrm_model, chooseholes=chooseholes) + + if rotdeg is not None: + self.rotdeg = rotdeg + + def read_nrm_model(self, nrm_model, chooseholes=None): + """ + + Calculate hole centers with appropriate rotation, + set these and other mask geometry details from NRMModel as + attributes. + + Parameters + ---------- + nrm_model: NRMModel + datamodel containing NRM reference file data + chooseholes: list + None, or e.g. ['B2', 'B4', 'B5', 'B6'] for a four-hole mask + + Returns + ------- + f2f: float + flat-to-flat distance of mask holes + ctrs_asbuilt: array + Actual hole centers [meters] + """ + + ctrs_asdesigned = np.array([[nrm_model.x_a1, nrm_model.y_a1], # B4 -> B4 + [nrm_model.x_a2, nrm_model.y_a2], # C5 -> C2 + [nrm_model.x_a3, nrm_model.y_a3], # B3 -> B5 + [nrm_model.x_a4, nrm_model.y_a4], # B6 -> B2 + [nrm_model.x_a5, nrm_model.y_a5], # C6 -> C1 + [nrm_model.x_a6, nrm_model.y_a6], # B2 -> B6 + [nrm_model.x_a7, nrm_model.y_a7]]) # C1 -> C6 self.hdia = nrm_model.flat_to_flat self.activeD = nrm_model.diameter self.OD = nrm_model.pupil_circumscribed - holedict = {} # as_built names, C2 open, C5 closed, but as designed coordinates - # Assemble holes by actual open segment names (as_built). Either the full mask or the - # subset-of-holes mask will be V2-reversed after the as_designed centers are defined - # Debug orientations with b4,c6,[c2] - allholes = ('b4', 'c2', 'b5', 'b2', 'c1', 'b6', 'c6') + holedict = {} # as_built names, C2 open, C5 closed, but as designed coordinates + # Assemble holes by actual open segment names (as_built). Either the full mask or the + # subset-of-holes mask will be V2-reversed after the as_designed centers are defined + # Debug orientations with b4,c6,[c2] + allholes = ('b4', 'c2', 'b5', 'b2', 'c1', 'b6', 'c6') - for hole, coords in zip(allholes,ctrs_asdesigned): - holedict[hole] = coords + for hole, coords in zip(allholes,ctrs_asdesigned): + holedict[hole] = coords - if chooseholes: # holes B4 B5 C6 asbuilt for orientation testing - holelist = [] - for h in allholes: - if h in chooseholes: - holelist.append(holedict[h]) - ctrs_asdesigned = np.array(holelist) + if chooseholes: # holes B4 B5 C6 asbuilt for orientation testing + holelist = [] + for h in allholes: + if h in chooseholes: + holelist.append(holedict[h]) + ctrs_asdesigned = np.array(holelist) - ctrs_asbuilt = ctrs_asdesigned.copy() + ctrs_asbuilt = 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 unaffected.... - ctrs_asbuilt[:, 0] *= -1 + # 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 unaffected.... + 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 pattern with the hex analytic FT, - # flat top & bottom as seen in DMS data. 8/2018 AS - ctrs_asbuilt = rotate2dccw(ctrs_asbuilt, np.pi / 2.0) # overwrites attributes + # 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 pattern with the hex analytic FT, + # flat top & bottom as seen in DMS data. 8/2018 AS + ctrs_asbuilt = rotate2dccw(ctrs_asbuilt, np.pi / 2.0) # overwrites attributes - # create 'live' hole centers in an ideal, orthogonal undistorted xy pupil space, - self.ctrs = ctrs_asbuilt + # create 'live' hole centers in an ideal, orthogonal undistorted xy pupil space, + self.ctrs = ctrs_asbuilt - def showmask(self): - """ - Calculate the diameter of the smallest centered circle (D) - enclosing the live mask area + def showmask(self): + """ + Calculate the diameter of the smallest centered circle (D) + enclosing the live mask area - Parameters - ---------- + Parameters + ---------- - Returns - ------- - Diameter of the smallest centered circle + Returns + ------- + Diameter of the smallest centered circle - """ - radii = [] - for ctr in self.ctrs: - radii.append(math.sqrt(ctr[0] * ctr[0] + ctr[1] * ctr[1])) + """ + radii = [] + for ctr in self.ctrs: + radii.append(math.sqrt(ctr[0] * ctr[0] + ctr[1] * ctr[1])) - return 2.0 * (max(radii) + 0.5 * self.hdia) + return 2.0 * (max(radii) + 0.5 * self.hdia) From 40f1e7247c308edd0d66c27781c14d497e87e508 Mon Sep 17 00:00:00 2001 From: Rachel Cooper Date: Wed, 20 Nov 2024 13:55:09 -0500 Subject: [PATCH 06/23] Clean up, add log messages about affine --- jwst/ami/ami_analyze.py | 5 +++++ jwst/ami/ami_analyze_step.py | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/jwst/ami/ami_analyze.py b/jwst/ami/ami_analyze.py index 45379d594a..31659179c9 100644 --- a/jwst/ami/ami_analyze.py +++ b/jwst/ami/ami_analyze.py @@ -179,6 +179,11 @@ def apply_LG_plus( # the affine2d returned here has only rotation... # to use rotation and scaling/shear, do some matrix multiplication here?? + log.info('Using affine transform with parameters:') + log.info(f'\tmx={affine2d.mx}\tmy={affine2d.my}') + log.info(f'\tsx={affine2d.sx}\tsy={affine2d.sy}') + log.info(f'\txo={affine2d.xo}\tyo={affine2d.yo}') + niriss = instrument_data.NIRISS(filt, nrm_model, bandpass=bandpass, diff --git a/jwst/ami/ami_analyze_step.py b/jwst/ami/ami_analyze_step.py index 2284ef9902..ec334aa890 100755 --- a/jwst/ami/ami_analyze_step.py +++ b/jwst/ami/ami_analyze_step.py @@ -180,16 +180,16 @@ def process(self, input): # If there's a user-defined bandpass or affine, handle it if bandpass is not None: bandpass = self.override_bandpass() - if affine2d is not None: if affine2d == 'commissioning': - affine2d = utils.Affine2d(mx= 9.92820e-01, + affine2d = utils.Affine2d(mx=9.92820e-01, my=9.98540e-01, sx=6.18605e-03, sy=-7.27008e-03, xo=0, yo=0, - name='commisioning') + name='commissioning') + self.log.info("Using affine parameters from commissioning.") else: affine2d = self.override_affine2d() # and if it is None, rotation search done in apply_LG_plus From 8cbbfc0cf9b1a9e422fe6dd23530c14ab5ef46e3 Mon Sep 17 00:00:00 2001 From: Rachel Cooper Date: Wed, 20 Nov 2024 17:20:03 -0500 Subject: [PATCH 07/23] Rename lg_model.NrmModel to lg_model.LgModel to avoid confusion with stdatamodels NRMModel --- jwst/ami/find_affine2d_parameters.py | 2 +- jwst/ami/lg_model.py | 10 +++++----- jwst/ami/nrm_core.py | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/jwst/ami/find_affine2d_parameters.py b/jwst/ami/find_affine2d_parameters.py index f21609e92e..6bdc74e826 100644 --- a/jwst/ami/find_affine2d_parameters.py +++ b/jwst/ami/find_affine2d_parameters.py @@ -98,7 +98,7 @@ def find_rotation(imagedata, psf_offset, rotdegs, mx, my, sx, sy, xo, yo, crosscorr_rots = [] for (rot, aff) in zip(rotdegs, affine2d_list): - jw = lg_model.NrmModel(mask='jwst_g7s6c', holeshape=holeshape, over=over, affine2d=aff) + jw = lg_model.LgModel(mask='jwst_ami', holeshape=holeshape, over=over, affine2d=aff) jw.set_pixelscale(pixel) # psf_offset in data coords & pixels. Does it get rotated? Second order errors poss. diff --git a/jwst/ami/lg_model.py b/jwst/ami/lg_model.py index 44d94a6194..b52e0f8285 100755 --- a/jwst/ami/lg_model.py +++ b/jwst/ami/lg_model.py @@ -19,7 +19,7 @@ mas = 1.0e-3 / (60 * 60 * 180 / np.pi) # in radians -class NrmModel: +class LgModel: """ A class for conveniently dealing with an "NRM object" This should be able to take an NRM_definition object for mask geometry. @@ -47,7 +47,7 @@ def __init__( **kwargs, ): """ - Set attributes of NrmModel class. + Set attributes of LgModel class. Parameters ---------- @@ -93,7 +93,7 @@ def __init__( # get these from mask_definition_ami instead if mask is None: - log.info("Using JWST AMI mask geometry from NrmModel") + log.info("Using JWST AMI mask geometry from LgModel") mask = mask_definition_ami.NRM_definition( maskname="jwst_ami", chooseholes=chooseholes ) @@ -122,7 +122,7 @@ def __init__( self.chooseholes = chooseholes - # affine2d property not to be changed in NrmModel - create a new + # affine2d property not to be changed in LgModel - create a new # instance instead # Save affine deformation of pupil object or create a no-deformation # object. @@ -400,7 +400,7 @@ def fit_image( def create_modelpsf(self): """ Make an image from the object's model and fit solutions, by setting the - NrmModel object's modelpsf attribute + LgModel object's modelpsf attribute Parameters ---------- diff --git a/jwst/ami/nrm_core.py b/jwst/ami/nrm_core.py index e8e54b68f7..8ae1fdc58b 100755 --- a/jwst/ami/nrm_core.py +++ b/jwst/ami/nrm_core.py @@ -170,7 +170,7 @@ def fit_fringes_single_integration(self, slc): Returns ------- - nrm: NrmModel object + nrm: LgModel object Model with best fit results Notes @@ -190,7 +190,7 @@ def fit_fringes_single_integration(self, slc): """ - nrm = lg_model.NrmModel(mask=self.instrument_data.mask, + nrm = lg_model.LgModel(mask=self.instrument_data.mask, pixscale=self.instrument_data.pscale_rad, holeshape=self.instrument_data.holeshape, affine2d=self.instrument_data.affine2d, From a7d7d4fa9dad56243b4b6834deece414218c4693 Mon Sep 17 00:00:00 2001 From: Rachel Cooper Date: Wed, 20 Nov 2024 17:35:48 -0500 Subject: [PATCH 08/23] default affine2d updated to 'commissioning' (special case) --- jwst/ami/ami_analyze_step.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jwst/ami/ami_analyze_step.py b/jwst/ami/ami_analyze_step.py index ec334aa890..b35ea00970 100755 --- a/jwst/ami/ami_analyze_step.py +++ b/jwst/ami/ami_analyze_step.py @@ -24,7 +24,7 @@ class AmiAnalyzeStep(Step): usebp = boolean(default=True) # If True, exclude pixels marked DO_NOT_USE from fringe fitting firstfew = integer(default=None) # If not None, process only the first few integrations chooseholes = string(default=None) # If not None, fit only certain fringes e.g. ['B4','B5','B6','C2'] - affine2d = string(default=None) # ASDF file containing user-defined affine parameters + affine2d = string(default='commissioning') # ASDF file containing user-defined affine parameters OR 'commssioning' run_bpfix = boolean(default=True) # Run Fourier bad pixel fix on cropped data """ From 1f296f17da18b6257e1719ce8fa585a39831c361 Mon Sep 17 00:00:00 2001 From: Rachel Cooper Date: Tue, 26 Nov 2024 16:33:35 -0500 Subject: [PATCH 09/23] change log --- changes/8974.ami.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/8974.ami.rst diff --git a/changes/8974.ami.rst b/changes/8974.ami.rst new file mode 100644 index 0000000000..ea39be8806 --- /dev/null +++ b/changes/8974.ami.rst @@ -0,0 +1 @@ +Use mask and pupil geometry constants from NRM reference file, and apply affine distortion from commissioning to LG model as default. \ No newline at end of file From c432d0a71141381b03091120e712831673bcc639 Mon Sep 17 00:00:00 2001 From: Rachel Cooper Date: Fri, 6 Dec 2024 16:24:21 -0500 Subject: [PATCH 10/23] removed old mask_definitions.py --- jwst/ami/mask_definitions.py | 179 ----------------------------------- 1 file changed, 179 deletions(-) delete mode 100755 jwst/ami/mask_definitions.py diff --git a/jwst/ami/mask_definitions.py b/jwst/ami/mask_definitions.py deleted file mode 100755 index d761f90c34..0000000000 --- a/jwst/ami/mask_definitions.py +++ /dev/null @@ -1,179 +0,0 @@ -# -# Module for defining mask geometry in pupil space -# - -import numpy as np -import math - -from .utils import rotate2dccw - -m = 1.0 -mm = 1.0e-3 * m -um = 1.0e-6 * m - - -class NRM_mask_definitions(): - - def __init__(self, maskname=None, rotdeg=None, holeshape="circ", rescale=False, - chooseholes=None): - """ - Set attributes of NRM_mask_definitions class. - - Parameters - ---------- - maskname: string - name of mask - - rotdeg: list of floats - range of rotations to search (degrees) - - holeshape: string - shape of apertures - - rescale: float - multiplicative factor to adjust hole sizes and centers in entrance pupil - - chooseholes: list - None, or e.g. ['B2', 'B4', 'B5', 'B6'] for a four-hole mask - - Returns - ------- - None - """ - - if maskname not in ["gpi_g10s40", "jwst_g7s6", "jwst_g7s6c", "visir_sam", - "p1640", "keck_nirc2", "pharo", "NIRC2_9NRM"]: - raise ValueError("mask not supported") - if holeshape is None: - holeshape = 'circ' - - if holeshape not in ["circ", "hex", ]: - raise ValueError("Unsupported mask holeshape" + maskname) - self.maskname = maskname - - if self.maskname == "jwst_g7s6c": - # activeD and D taken from webbpsf-data/NIRISS/coronagraph/MASK_NRM.fits - - self.hdia, self.ctrs = jwst_g7s6c(chooseholes=chooseholes) - self.activeD = 6.559 * m # webbpsf kwd DIAM - not a 'circle including all holes' - self.OD = 6.610645669291339 * m # Full pupil file size, incl padding, webbpsf kwd PUPLDIAM - if rotdeg is not None: - self.rotdeg = rotdeg - - elif self.maskname == "jwst_g7s6": - pass # not finished - - def showmask(self): - """ - Calculate the diameter of the smallest centered circle (D) - enclosing the live mask area - - Parameters - ---------- - - Returns - ------- - Diameter of the smallest centered circle - - """ - radii = [] - for ctr in self.ctrs: - radii.append(math.sqrt(ctr[0] * ctr[0] + ctr[1] * ctr[1])) - - return 2.0 * (max(radii) + 0.5 * self.hdia) - - -def jwst_g7s6_centers_asbuilt(chooseholes=None): # was jwst_g7s6_centers_asdesigned - """ - Calculate hole centers with appropriate rotation - - Parameters - ---------- - chooseholes: list - None, or e.g. ['B2', 'B4', 'B5', 'B6'] for a four-hole mask - - Returns - ------- - ctrs_asbuilt: array - Actual hole centers [meters] - """ - - holedict = {} # as_built names, C2 open, C5 closed, but as designed coordinates - # Assemble holes by actual open segment names (as_built). Either the full mask or the - # subset-of-holes mask will be V2-reversed after the as_designed centers are defined - # Debug orientations with b4,c6,[c2] - allholes = ('b4', 'c2', 'b5', 'b2', 'c1', 'b6', 'c6') - - # design built - 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 - - # 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: # holes B4 B5 C6 asbuilt for orientation testing - holelist = [] - for h in allholes: - if h in chooseholes: - holelist.append(holedict[h]) - ctrs_asdesigned = np.array(holelist) - else: - # the REAL THING - as_designed 7 hole, m in PM space, no distortion - # ... shape (7,2) - 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 - - # Preserve ctrs.as-designed (treat as immutable) - # Reverse V2 axis coordinates to close C5 open C2, and others follow suit... - # preserve cts.as_built (treat as immutable) - ctrs_asbuilt = 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 unaffected.... - 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 pattern with the hex analytic FT, - # flat top & bottom as seen in DMS data. 8/2018 AS - ctrs_asbuilt = rotate2dccw(ctrs_asbuilt, np.pi / 2.0) # overwrites attributes - - # create 'live' hole centers in an ideal, orthogonal undistorted xy pupil space, - return ctrs_asbuilt * m - - -def jwst_g7s6c(chooseholes=None): - """ - - Calculate hole centers with appropriate rotation - - Parameters - ---------- - chooseholes: list - None, or e.g. ['B2', 'B4', 'B5', 'B6'] for a four-hole mask - - Returns - ------- - f2f: float - flat-to-flat distance of mask holes - ctrs_asbuilt: array - Actual hole centers [meters] - - """ - - f2f = 0.8 * m # m flat to flat - return f2f, jwst_g7s6_centers_asbuilt(chooseholes=chooseholes) From 0b435a3a44626ec430df39711daf9d6681ab2937 Mon Sep 17 00:00:00 2001 From: Rachel Cooper Date: Mon, 9 Dec 2024 13:35:01 -0500 Subject: [PATCH 11/23] Add special affine2d default 'commissioning' to documentation --- docs/jwst/ami_analyze/description.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/jwst/ami_analyze/description.rst b/docs/jwst/ami_analyze/description.rst index 26b2f597ff..36e9b6622c 100644 --- a/docs/jwst/ami_analyze/description.rst +++ b/docs/jwst/ami_analyze/description.rst @@ -47,11 +47,14 @@ other options: :--chooseholes: If not None, fit only certain fringes e.g. ['B4','B5','B6','C2'] (default=None) -:--affine2d: ASDF file containing user-defined affine parameters (default=None) +:--affine2d: ASDF file containing user-defined affine parameters (default='commissioning') :--run_bpfix: Run Fourier bad pixel fix on cropped data (default=True) +Note that the `affine2d` default argument is a special case; 'commissioning' is currently the only string other than an ASDF filename that is accepted. + + Creating ASDF files ^^^^^^^^^^^^^^^^^^^ The optional arguments `bandpass` and `affine2d` must be written to `ASDF `_ From ac8915ffdd191ecd70e70a4e8ac92a4499878ec3 Mon Sep 17 00:00:00 2001 From: Rachel Cooper Date: Mon, 9 Dec 2024 14:28:04 -0500 Subject: [PATCH 12/23] docstring updates, update test to use correct datamodel meta structure --- jwst/ami/mask_definition_ami.py | 16 +++++----------- jwst/ami/tests/test_ami_interface.py | 4 ++-- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/jwst/ami/mask_definition_ami.py b/jwst/ami/mask_definition_ami.py index 04cfe730f7..59a33c44a0 100644 --- a/jwst/ami/mask_definition_ami.py +++ b/jwst/ami/mask_definition_ami.py @@ -23,17 +23,14 @@ def __init__(self, nrm_model, maskname='jwst_ami', rotdeg=None, chooseholes=None ---------- nrm_model: NRMModel datamodel containing NRM reference file data - + maskname: string + Identifier for mask geometry; default 'jwst_ami', optional rotdeg: list of floats - range of rotations to search (degrees) - + range of rotations to search (degrees), optional chooseholes: list - None, or e.g. ['B2', 'B4', 'B5', 'B6'] for a four-hole mask - - Returns - ------- - None + None, or e.g. ['B2', 'B4', 'B5', 'B6'] for a four-hole mask, optional """ + if maskname not in ['jwst_ami','jwst_g7s6c']: raise ValueError("Mask name not supported") @@ -114,9 +111,6 @@ def showmask(self): Calculate the diameter of the smallest centered circle (D) enclosing the live mask area - Parameters - ---------- - Returns ------- Diameter of the smallest centered circle diff --git a/jwst/ami/tests/test_ami_interface.py b/jwst/ami/tests/test_ami_interface.py index 23e765a343..1b64801d25 100644 --- a/jwst/ami/tests/test_ami_interface.py +++ b/jwst/ami/tests/test_ami_interface.py @@ -48,8 +48,8 @@ def example_model(mock_nrm_reference_file): model.meta.program.pi_name = "someone" model.meta.target.catalog_name = "" model.meta.visit.start_time = "2022-06-05 12:15:41.5020000" - model.meta.wcsinfo.roll_ref = 171.8779402866089 - model.meta.wcsinfo.v3yangle = 0.56126717 + model.meta.ami.roll_ref = 171.8779402866089 + model.meta.ami.v3yangle = 0.56126717 model.meta.filename = "test_calints.fits" model.meta.instrument.pupil = "NRM" model.meta.exposure.type = "NIS_AMI" From 22656e69e3fb4633f04c8588f60e3119ce057828 Mon Sep 17 00:00:00 2001 From: Rachel Cooper Date: Mon, 9 Dec 2024 14:59:53 -0500 Subject: [PATCH 13/23] undo wrong 'fix' to test --- jwst/ami/tests/test_ami_interface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jwst/ami/tests/test_ami_interface.py b/jwst/ami/tests/test_ami_interface.py index 1b64801d25..23e765a343 100644 --- a/jwst/ami/tests/test_ami_interface.py +++ b/jwst/ami/tests/test_ami_interface.py @@ -48,8 +48,8 @@ def example_model(mock_nrm_reference_file): model.meta.program.pi_name = "someone" model.meta.target.catalog_name = "" model.meta.visit.start_time = "2022-06-05 12:15:41.5020000" - model.meta.ami.roll_ref = 171.8779402866089 - model.meta.ami.v3yangle = 0.56126717 + model.meta.wcsinfo.roll_ref = 171.8779402866089 + model.meta.wcsinfo.v3yangle = 0.56126717 model.meta.filename = "test_calints.fits" model.meta.instrument.pupil = "NRM" model.meta.exposure.type = "NIS_AMI" From a472b26335e40a55acdca61d4079951e0f16ff94 Mon Sep 17 00:00:00 2001 From: Rachel Cooper Date: Mon, 9 Dec 2024 15:53:21 -0500 Subject: [PATCH 14/23] Remove use of mock_nrm_reference_file since it exists on crds ops now --- jwst/ami/tests/test_ami_interface.py | 25 +------------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/jwst/ami/tests/test_ami_interface.py b/jwst/ami/tests/test_ami_interface.py index 23e765a343..4e9f9675b5 100644 --- a/jwst/ami/tests/test_ami_interface.py +++ b/jwst/ami/tests/test_ami_interface.py @@ -10,30 +10,7 @@ @pytest.fixture() -def mock_nrm_reference_file(tmp_path, monkeypatch): - """ - At the moment nrm only exists on CRDS-TEST so this - fixture will mock fetching any 'nrm' reference file - """ - fn = tmp_path / "nrm.fits" - - # make a fake nrm file - m = datamodels.NRMModel() - m.nrm = np.zeros((1024, 1024), dtype='f4') - m.save(fn) - - original_get_reference_file = crds_client.get_reference_file - - def mock_get_reference_file(dataset, reference_file_type, observatory): - if reference_file_type == 'nrm': - return str(fn) - return original_get_reference_file(dataset, reference_file_type, observatory) - - monkeypatch.setattr(crds_client, "get_reference_file", mock_get_reference_file) - - -@pytest.fixture() -def example_model(mock_nrm_reference_file): +def example_model(): model = datamodels.CubeModel((2, 80, 80)) # some non-zero data is required as this step will center # the image and find the centroid (both fail with all zeros) From 3d4fb46e967cd08489f5ccf7e0f08eb9e9572a80 Mon Sep 17 00:00:00 2001 From: Rachel Cooper Date: Mon, 9 Dec 2024 17:02:31 -0500 Subject: [PATCH 15/23] fix FutureWarning in linalg.lstsq --- jwst/ami/leastsqnrm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jwst/ami/leastsqnrm.py b/jwst/ami/leastsqnrm.py index 79983d34e7..ec31a48680 100644 --- a/jwst/ami/leastsqnrm.py +++ b/jwst/ami/leastsqnrm.py @@ -490,7 +490,7 @@ def weighted_operations(img, model, dqm=None): Aw = flatmodel * weights[:, np.newaxis] bw = flatimg * weights # resids are pixel value residuals, flattened to 1d vector - x, rss, rank, singvals = np.linalg.lstsq(Aw, bw) + x, rss, rank, singvals = np.linalg.lstsq(Aw, bw, rcond=None) # actual residuals in image: res = flatimg - np.dot(flatmodel, x) From 761e18296822814891651a42b0479b1b44f98c9f Mon Sep 17 00:00:00 2001 From: Rachel Cooper Date: Tue, 10 Dec 2024 17:53:27 -0500 Subject: [PATCH 16/23] pass NRMModel to lg_model.LgModel so that find_affine2d_parameters.find_rotation() has mask info it needs when affine2d input is None. Could probably be cleaned up a bit --- jwst/ami/ami_analyze.py | 11 ++++++----- jwst/ami/find_affine2d_parameters.py | 7 +++++-- jwst/ami/instrument_data.py | 5 ++++- jwst/ami/lg_model.py | 12 ++++++++---- jwst/ami/nrm_core.py | 3 ++- 5 files changed, 25 insertions(+), 13 deletions(-) diff --git a/jwst/ami/ami_analyze.py b/jwst/ami/ami_analyze.py index 31659179c9..4277bac9a8 100644 --- a/jwst/ami/ami_analyze.py +++ b/jwst/ami/ami_analyze.py @@ -126,9 +126,8 @@ def apply_LG_plus( # Throughput (combined filter and source spectrum) calculated here bandpass = utils.handle_bandpass(bandpass, throughput_model) - - if affine2d is None: + log.info("Searching for best-fit affine transform") rotsearch_d = np.append( np.arange( rotsearch_parameters[0], rotsearch_parameters[1], rotsearch_parameters[2] @@ -161,6 +160,7 @@ def apply_LG_plus( affine2d = find_rotation( meddata, + nrm_model, psf_offset, rotsearch_d, mx, @@ -180,9 +180,10 @@ def apply_LG_plus( # to use rotation and scaling/shear, do some matrix multiplication here?? log.info('Using affine transform with parameters:') - log.info(f'\tmx={affine2d.mx}\tmy={affine2d.my}') - log.info(f'\tsx={affine2d.sx}\tsy={affine2d.sy}') - log.info(f'\txo={affine2d.xo}\tyo={affine2d.yo}') + log.info(f'\tmx={affine2d.mx:.6f}\tmy={affine2d.my:.6f}') + log.info(f'\tsx={affine2d.sx:.6f}\tsy={affine2d.sy:.6f}') + log.info(f'\txo={affine2d.xo:.6f}\tyo={affine2d.yo:.6f}') + log.info(f'\trotradccw={affine2d.rotradccw:.4f}') niriss = instrument_data.NIRISS(filt, nrm_model, diff --git a/jwst/ami/find_affine2d_parameters.py b/jwst/ami/find_affine2d_parameters.py index 6bdc74e826..0ba18090ab 100644 --- a/jwst/ami/find_affine2d_parameters.py +++ b/jwst/ami/find_affine2d_parameters.py @@ -35,7 +35,7 @@ def create_afflist_rot(rotdegs): return alist -def find_rotation(imagedata, psf_offset, rotdegs, mx, my, sx, sy, xo, yo, +def find_rotation(imagedata, nrm_model, psf_offset, rotdegs, mx, my, sx, sy, xo, yo, pixel, npix, bandpass, over, holeshape): """ Create an affine2d object using the known rotation and scale. @@ -45,6 +45,9 @@ def find_rotation(imagedata, psf_offset, rotdegs, mx, my, sx, sy, xo, yo, imagedata: 2D float array image data + nrm_model: NRMModel datamodel + datamodel containing mask geometry information + psf_offset: 2D float array offset from image center in detector pixels @@ -98,7 +101,7 @@ def find_rotation(imagedata, psf_offset, rotdegs, mx, my, sx, sy, xo, yo, crosscorr_rots = [] for (rot, aff) in zip(rotdegs, affine2d_list): - jw = lg_model.LgModel(mask='jwst_ami', holeshape=holeshape, over=over, affine2d=aff) + jw = lg_model.LgModel(nrm_model, mask='jwst_ami', holeshape=holeshape, over=over, affine2d=aff) jw.set_pixelscale(pixel) # psf_offset in data coords & pixels. Does it get rotated? Second order errors poss. diff --git a/jwst/ami/instrument_data.py b/jwst/ami/instrument_data.py index 08d30bdc6e..f2c41cff86 100755 --- a/jwst/ami/instrument_data.py +++ b/jwst/ami/instrument_data.py @@ -38,6 +38,9 @@ def __init__(self, ---------- filt: string filter name + + nrm_model: NRMModel datamodel + datamodel containing mask geometry information chooseholes: list None, or e.g. ['B2', 'B4', 'B5', 'B6'] for a four-hole mask @@ -94,7 +97,7 @@ def __init__(self, self.arrname = "jwst_ami" self.holeshape = 'hex' self.mask = NRM_definition( - nrm_model, + self.nrm_model, maskname=self.arrname, chooseholes=self.chooseholes ) diff --git a/jwst/ami/lg_model.py b/jwst/ami/lg_model.py index b52e0f8285..598df552ca 100755 --- a/jwst/ami/lg_model.py +++ b/jwst/ami/lg_model.py @@ -27,13 +27,14 @@ class LgModel: Simulates PSF (broadband or monochromatic) Builds a fringe model - either by user definition, or automated to data Fits model to data by least squares - Masks: gpi_g10s40, jwst, visir + Masks: jwst_ami (formerly jwst_g7s6c) Algorithm documented in Greenbaum, A. Z., Pueyo, L. P., Sivaramakrishnan, A., and Lacour, S., Astrophysical Journal vol. 798, Jan 2015. """ def __init__( self, + nrm_model, mask=None, holeshape="hex", pixscale=None, @@ -51,6 +52,9 @@ def __init__( Parameters ---------- + nrm_model: NRMModel datamodel + datamodel containing mask geometry information + mask: string keyword for built-in values @@ -85,7 +89,7 @@ def __init__( self.debug = kwargs["debug"] else: self.debug = False - + self.holeshape = holeshape self.pixel = pixscale # det pix in rad (square) self.over = over @@ -94,11 +98,11 @@ def __init__( # get these from mask_definition_ami instead if mask is None: log.info("Using JWST AMI mask geometry from LgModel") - mask = mask_definition_ami.NRM_definition( + mask = mask_definition_ami.NRM_definition(nrm_model, maskname="jwst_ami", chooseholes=chooseholes ) elif isinstance(mask, str): - mask = mask_definition_ami.NRM_definition( + mask = mask_definition_ami.NRM_definition(nrm_model, maskname=mask, chooseholes=chooseholes ) # retain ability to possibly use other named masks, for now self.ctrs = mask.ctrs diff --git a/jwst/ami/nrm_core.py b/jwst/ami/nrm_core.py index 8ae1fdc58b..e03c911895 100755 --- a/jwst/ami/nrm_core.py +++ b/jwst/ami/nrm_core.py @@ -190,7 +190,8 @@ def fit_fringes_single_integration(self, slc): """ - nrm = lg_model.LgModel(mask=self.instrument_data.mask, + nrm = lg_model.LgModel(self.instrument_data.nrm_model, + mask=self.instrument_data.mask, pixscale=self.instrument_data.pscale_rad, holeshape=self.instrument_data.holeshape, affine2d=self.instrument_data.affine2d, From 77bfec6510f7ef0c18825dbe3479347ce8e1568e Mon Sep 17 00:00:00 2001 From: Rachel Cooper Date: Tue, 10 Dec 2024 18:00:39 -0500 Subject: [PATCH 17/23] a little cleanup of lgmodel inputs --- jwst/ami/lg_model.py | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/jwst/ami/lg_model.py b/jwst/ami/lg_model.py index 598df552ca..c80e5f65ca 100755 --- a/jwst/ami/lg_model.py +++ b/jwst/ami/lg_model.py @@ -40,10 +40,8 @@ def __init__( pixscale=None, over=1, pixweight=None, - datapath="", phi=None, - refdir="", - chooseholes=False, + chooseholes=None, affine2d=None, **kwargs, ): @@ -59,7 +57,7 @@ def __init__( keyword for built-in values holeshape: string - shape of apertures + shape of apertures, default="hex" pixscale: float initial estimate of pixel scale in radians @@ -70,17 +68,11 @@ def __init__( pixweight: 2D float array, default is None weighting array - datapath: string - directory for output (will remove for final) - phi: float 1D array distance of fringe from hole center in units of waves - refdir: string - directory containing ref files (will remove for final) - - chooseholes: list ? - holes ...? + chooseholes: list of strings + None, or e.g. ['B2', 'B4', 'B5', 'B6'] for a four-hole mask affine2d: Affine2d object Affine2d object @@ -89,7 +81,7 @@ def __init__( self.debug = kwargs["debug"] else: self.debug = False - + self.holeshape = holeshape self.pixel = pixscale # det pix in rad (square) self.over = over @@ -110,11 +102,9 @@ def __init__( self.D = mask.activeD self.N = len(self.ctrs) - self.datapath = datapath - self.refdir = refdir self.fmt = "%10.4e" - # get latest OPD from WebbPSF? + # get closest in time OPD from WebbPSF? if phi: # meters of OPD at central wavelength if phi == "perfect": From 07998646c0ac4d0e52954f837d1214a2b043436c Mon Sep 17 00:00:00 2001 From: Rachel Cooper Date: Tue, 10 Dec 2024 18:23:32 -0500 Subject: [PATCH 18/23] remove now-unused imports --- jwst/ami/tests/test_ami_interface.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/jwst/ami/tests/test_ami_interface.py b/jwst/ami/tests/test_ami_interface.py index 4e9f9675b5..b4ccd7c081 100644 --- a/jwst/ami/tests/test_ami_interface.py +++ b/jwst/ami/tests/test_ami_interface.py @@ -1,8 +1,6 @@ -import numpy as np import pytest import stpipe -from stpipe import crds_client from stdatamodels.jwst import datamodels From cd3524f644f9d70da03ff9fac154d0f7e388b562 Mon Sep 17 00:00:00 2001 From: Rachel Cooper Date: Wed, 11 Dec 2024 12:03:07 -0500 Subject: [PATCH 19/23] try to handle None input interpreted as string --- jwst/ami/ami_analyze_step.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/jwst/ami/ami_analyze_step.py b/jwst/ami/ami_analyze_step.py index b35ea00970..3dc95529bc 100755 --- a/jwst/ami/ami_analyze_step.py +++ b/jwst/ami/ami_analyze_step.py @@ -157,6 +157,11 @@ def process(self, input): # pull out parameters that are strings and change to floats psf_offset = [float(a) for a in self.psf_offset.split()] rotsearch_parameters = [float(a) for a in self.rotation_search.split()] + # handle command-line None input interpreted as string + if affine2d in ["None", "none"]: + affine2d = None + if bandpass in ["None", "none"]: + bandpass = None self.log.info(f"Oversampling factor = {oversample}") self.log.info(f"Initial rotation guess = {rotate} deg") From 85ad403b88c734c4fe3d461bc9fd606675666ed3 Mon Sep 17 00:00:00 2001 From: Rachel Cooper Date: Wed, 11 Dec 2024 14:33:54 -0500 Subject: [PATCH 20/23] Fix break caused by log statement --- jwst/ami/ami_analyze_step.py | 1 + 1 file changed, 1 insertion(+) diff --git a/jwst/ami/ami_analyze_step.py b/jwst/ami/ami_analyze_step.py index 3dc95529bc..dd57ce894d 100755 --- a/jwst/ami/ami_analyze_step.py +++ b/jwst/ami/ami_analyze_step.py @@ -193,6 +193,7 @@ def process(self, input): sy=-7.27008e-03, xo=0, yo=0, + rotradccw=0, name='commissioning') self.log.info("Using affine parameters from commissioning.") else: From bc48f11cfe0ab55b24292e4bdb482ca17b4b001b Mon Sep 17 00:00:00 2001 From: Rachel Cooper Date: Thu, 12 Dec 2024 14:26:11 -0500 Subject: [PATCH 21/23] Style and doctring updates following review --- docs/jwst/ami_analyze/description.rst | 2 +- jwst/ami/ami_analyze_step.py | 5 ++--- jwst/ami/instrument_data.py | 4 ++-- jwst/ami/lg_model.py | 13 +++++++------ jwst/ami/mask_definition_ami.py | 21 ++++++++++++--------- 5 files changed, 24 insertions(+), 21 deletions(-) diff --git a/docs/jwst/ami_analyze/description.rst b/docs/jwst/ami_analyze/description.rst index 36e9b6622c..2e4ec08636 100644 --- a/docs/jwst/ami_analyze/description.rst +++ b/docs/jwst/ami_analyze/description.rst @@ -52,7 +52,7 @@ other options: :--run_bpfix: Run Fourier bad pixel fix on cropped data (default=True) -Note that the `affine2d` default argument is a special case; 'commissioning' is currently the only string other than an ASDF filename that is accepted. +Note that the `affine2d` default argument is a special case; 'commissioning' is currently the only string other than an ASDF filename that is accepted. If `None` is passed, it will perform a rotation search (least-squares fit to a PSF model) and use that for the affine transform. Creating ASDF files diff --git a/jwst/ami/ami_analyze_step.py b/jwst/ami/ami_analyze_step.py index dd57ce894d..d5c009db7e 100755 --- a/jwst/ami/ami_analyze_step.py +++ b/jwst/ami/ami_analyze_step.py @@ -157,11 +157,10 @@ def process(self, input): # pull out parameters that are strings and change to floats psf_offset = [float(a) for a in self.psf_offset.split()] rotsearch_parameters = [float(a) for a in self.rotation_search.split()] + # handle command-line None input interpreted as string - if affine2d in ["None", "none"]: + if str(affine2d).lower() == 'none': affine2d = None - if bandpass in ["None", "none"]: - bandpass = None self.log.info(f"Oversampling factor = {oversample}") self.log.info(f"Initial rotation guess = {rotate} deg") diff --git a/jwst/ami/instrument_data.py b/jwst/ami/instrument_data.py index f2c41cff86..cbc253eabf 100755 --- a/jwst/ami/instrument_data.py +++ b/jwst/ami/instrument_data.py @@ -6,7 +6,7 @@ import logging import numpy as np -from .mask_definition_ami import NRM_definition +from .mask_definition_ami import NRMDefinition from . import utils from . import bp_fix from stdatamodels.jwst.datamodels import dqflags @@ -96,7 +96,7 @@ def __init__(self, self.instrument = "NIRISS" self.arrname = "jwst_ami" self.holeshape = 'hex' - self.mask = NRM_definition( + self.mask = NRMDefinition( self.nrm_model, maskname=self.arrname, chooseholes=self.chooseholes diff --git a/jwst/ami/lg_model.py b/jwst/ami/lg_model.py index c80e5f65ca..29170738dd 100755 --- a/jwst/ami/lg_model.py +++ b/jwst/ami/lg_model.py @@ -22,7 +22,7 @@ class LgModel: """ A class for conveniently dealing with an "NRM object" This should be able - to take an NRM_definition object for mask geometry. + to take an NRMDefinition object for mask geometry. Defines mask geometry and detector-scale parameters. Simulates PSF (broadband or monochromatic) Builds a fringe model - either by user definition, or automated to data @@ -65,14 +65,15 @@ def __init__( over: integer oversampling factor - pixweight: 2D float array, default is None + pixweight: 2D float array, default None weighting array phi: float 1D array distance of fringe from hole center in units of waves - chooseholes: list of strings - None, or e.g. ['B2', 'B4', 'B5', 'B6'] for a four-hole mask + chooseholes: list of strings, default None + E.g. ['B2', 'B4', 'B5', 'B6'] for a four-hole mask + If None, use the real seven-hole mask. affine2d: Affine2d object Affine2d object @@ -90,11 +91,11 @@ def __init__( # get these from mask_definition_ami instead if mask is None: log.info("Using JWST AMI mask geometry from LgModel") - mask = mask_definition_ami.NRM_definition(nrm_model, + mask = mask_definition_ami.NRMDefinition(nrm_model, maskname="jwst_ami", chooseholes=chooseholes ) elif isinstance(mask, str): - mask = mask_definition_ami.NRM_definition(nrm_model, + mask = mask_definition_ami.NRMDefinition(nrm_model, maskname=mask, chooseholes=chooseholes ) # retain ability to possibly use other named masks, for now self.ctrs = mask.ctrs diff --git a/jwst/ami/mask_definition_ami.py b/jwst/ami/mask_definition_ami.py index 59a33c44a0..140dc539af 100644 --- a/jwst/ami/mask_definition_ami.py +++ b/jwst/ami/mask_definition_ami.py @@ -13,11 +13,14 @@ um = 1.0e-6 * m -class NRM_definition(): +class NRMDefinition(): def __init__(self, nrm_model, maskname='jwst_ami', rotdeg=None, chooseholes=None): """ - Set attributes of NRM_definition class. + Set attributes of NRMDefinition class. + + Get hole centers and other mask geometry details from NRMModel, apply rotations/flips + as necessary and set them as attributes. Parameters ---------- @@ -29,12 +32,17 @@ def __init__(self, nrm_model, maskname='jwst_ami', rotdeg=None, chooseholes=None range of rotations to search (degrees), optional chooseholes: list None, or e.g. ['B2', 'B4', 'B5', 'B6'] for a four-hole mask, optional + If None, use real seven-hole mask """ if maskname not in ['jwst_ami','jwst_g7s6c']: raise ValueError("Mask name not supported") self.maskname = maskname # there's only one mask but this is used in oifits + self.hdia = nrm_model.flat_to_flat + self.activeD = nrm_model.diameter + self.OD = nrm_model.pupil_circumscribed + self.ctrs = [] self.read_nrm_model(nrm_model, chooseholes=chooseholes) @@ -43,10 +51,7 @@ def __init__(self, nrm_model, maskname='jwst_ami', rotdeg=None, chooseholes=None def read_nrm_model(self, nrm_model, chooseholes=None): """ - - Calculate hole centers with appropriate rotation, - set these and other mask geometry details from NRMModel as - attributes. + Calculate hole centers with appropriate rotation. Parameters ---------- @@ -71,9 +76,7 @@ def read_nrm_model(self, nrm_model, chooseholes=None): [nrm_model.x_a6, nrm_model.y_a6], # B2 -> B6 [nrm_model.x_a7, nrm_model.y_a7]]) # C1 -> C6 - self.hdia = nrm_model.flat_to_flat - self.activeD = nrm_model.diameter - self.OD = nrm_model.pupil_circumscribed + holedict = {} # as_built names, C2 open, C5 closed, but as designed coordinates # Assemble holes by actual open segment names (as_built). Either the full mask or the From 9254e6711d80c20228c8f92193fba82097b3a939 Mon Sep 17 00:00:00 2001 From: Rachel Cooper Date: Thu, 12 Dec 2024 14:39:32 -0500 Subject: [PATCH 22/23] Fixed use of affine rotradccw, log statement with commissioning default --- jwst/ami/ami_analyze.py | 2 +- jwst/ami/ami_analyze_step.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/jwst/ami/ami_analyze.py b/jwst/ami/ami_analyze.py index 4277bac9a8..adb78e94eb 100644 --- a/jwst/ami/ami_analyze.py +++ b/jwst/ami/ami_analyze.py @@ -183,7 +183,7 @@ def apply_LG_plus( log.info(f'\tmx={affine2d.mx:.6f}\tmy={affine2d.my:.6f}') log.info(f'\tsx={affine2d.sx:.6f}\tsy={affine2d.sy:.6f}') log.info(f'\txo={affine2d.xo:.6f}\tyo={affine2d.yo:.6f}') - log.info(f'\trotradccw={affine2d.rotradccw:.4f}') + log.info(f'\trotradccw={affine2d.rotradccw}') niriss = instrument_data.NIRISS(filt, nrm_model, diff --git a/jwst/ami/ami_analyze_step.py b/jwst/ami/ami_analyze_step.py index d5c009db7e..19cf0791be 100755 --- a/jwst/ami/ami_analyze_step.py +++ b/jwst/ami/ami_analyze_step.py @@ -192,7 +192,6 @@ def process(self, input): sy=-7.27008e-03, xo=0, yo=0, - rotradccw=0, name='commissioning') self.log.info("Using affine parameters from commissioning.") else: From 1cc5a6c0fc05c5bee50b115251398757dbd8fa2d Mon Sep 17 00:00:00 2001 From: Rachel Cooper Date: Fri, 13 Dec 2024 11:30:34 -0500 Subject: [PATCH 23/23] Style updates from second review --- jwst/ami/ami_analyze.py | 3 +-- jwst/ami/ami_analyze_step.py | 19 +++++++++++------ jwst/ami/find_affine2d_parameters.py | 9 +++----- jwst/ami/instrument_data.py | 10 ++++----- jwst/ami/lg_model.py | 32 +++++++++++++++++----------- jwst/ami/mask_definition_ami.py | 17 +-------------- jwst/ami/nrm_core.py | 11 ++++------ jwst/ami/oifits.py | 21 +++++++++--------- 8 files changed, 57 insertions(+), 65 deletions(-) diff --git a/jwst/ami/ami_analyze.py b/jwst/ami/ami_analyze.py index adb78e94eb..b872f52951 100644 --- a/jwst/ami/ami_analyze.py +++ b/jwst/ami/ami_analyze.py @@ -1,4 +1,3 @@ -# Module for applying the LG-PLUS algorithm to an AMI exposure import logging import numpy as np import copy @@ -30,7 +29,7 @@ def apply_LG_plus( run_bpfix, ): """ - Applies the image plane algorithm to an AMI image + Applies the image plane algorithm (LG-PLUS) to an AMI exposure Parameters ---------- diff --git a/jwst/ami/ami_analyze_step.py b/jwst/ami/ami_analyze_step.py index 19cf0791be..52a8ca9b93 100755 --- a/jwst/ami/ami_analyze_step.py +++ b/jwst/ami/ami_analyze_step.py @@ -9,6 +9,13 @@ __all__ = ["AmiAnalyzeStep"] +# Affine parameters from commissioning +MX_COMM = 9.92820e-01 +MY_COMM = 9.98540e-01 +SX_COMM = 6.18605e-03 +SY_COMM = -7.27008e-03 +XO_COMM = 0. +YO_COMM = 0. class AmiAnalyzeStep(Step): """Performs analysis of an AMI mode exposure by applying the LG algorithm.""" @@ -186,12 +193,12 @@ def process(self, input): bandpass = self.override_bandpass() if affine2d is not None: if affine2d == 'commissioning': - affine2d = utils.Affine2d(mx=9.92820e-01, - my=9.98540e-01, - sx=6.18605e-03, - sy=-7.27008e-03, - xo=0, - yo=0, + affine2d = utils.Affine2d(mx=MX_COMM, + my=MY_COMM, + sx=SX_COMM, + sy=SY_COMM, + xo=XO_COMM, + yo=YO_COMM, name='commissioning') self.log.info("Using affine parameters from commissioning.") else: diff --git a/jwst/ami/find_affine2d_parameters.py b/jwst/ami/find_affine2d_parameters.py index 0ba18090ab..b554e7d336 100644 --- a/jwst/ami/find_affine2d_parameters.py +++ b/jwst/ami/find_affine2d_parameters.py @@ -1,7 +1,3 @@ -# -# Module for calculation of the optimal rotation for an image plane -# - import logging import numpy as np @@ -14,8 +10,9 @@ def create_afflist_rot(rotdegs): """ - Create a list of affine objects with various rotations to use in order to - go through and find which fits an image plane data best. + Create a list of affine objects with various rotations. + + Find which affine rotation fits an image plane data best. Parameters ---------- diff --git a/jwst/ami/instrument_data.py b/jwst/ami/instrument_data.py index cbc253eabf..3d317a7e7b 100755 --- a/jwst/ami/instrument_data.py +++ b/jwst/ami/instrument_data.py @@ -1,8 +1,3 @@ -# -# Module for defining data format, wavelength info, an mask geometry for these -# instrument: NIRISS AMI -# - import logging import numpy as np @@ -32,7 +27,10 @@ def __init__(self, run_bpfix=True ): """ - Initialize NIRISS class + Initialize NIRISS class for NIRISS/AMI instrument. + + Module for defining all instrument characteristics including data format, + wavelength info, and mask geometry. Parameters ---------- diff --git a/jwst/ami/lg_model.py b/jwst/ami/lg_model.py index 29170738dd..466fa6323c 100755 --- a/jwst/ami/lg_model.py +++ b/jwst/ami/lg_model.py @@ -1,5 +1,3 @@ -# A module for conveniently manipulating an 'NRM object' using the -# Lacour-Greenbaum algorithm. First written by Alexandra Greenbaum in 2014. import logging import numpy as np @@ -21,8 +19,9 @@ class LgModel: """ - A class for conveniently dealing with an "NRM object" This should be able - to take an NRMDefinition object for mask geometry. + A class for conveniently dealing with an "NRM object." + + This should be able to take an NRMDefinition object for mask geometry. Defines mask geometry and detector-scale parameters. Simulates PSF (broadband or monochromatic) Builds a fringe model - either by user definition, or automated to data @@ -30,6 +29,7 @@ class LgModel: Masks: jwst_ami (formerly jwst_g7s6c) Algorithm documented in Greenbaum, A. Z., Pueyo, L. P., Sivaramakrishnan, A., and Lacour, S., Astrophysical Journal vol. 798, Jan 2015. + First written by Alexandra Greenbaum in 2014. """ def __init__( @@ -132,8 +132,10 @@ def __init__( def simulate(self, fov=None, bandpass=None, over=None, psf_offset=(0, 0)): """ - Simulate a detector-scale psf using parameters input from the call and - already stored in the object,and generate a simulation fits header + Simulate a detector-scale psf. + + Use parameters input from the call and + already stored in the object, and generate a simulation fits header storing all of the parameters used to generate that psf. If the input bandpass is one number it will calculate a monochromatic psf. @@ -191,9 +193,11 @@ def make_model( self, fov=None, bandpass=None, over=1, psf_offset=(0, 0), pixscale=None ): """ - Generates the fringe model with the attributes of the object using a - bandpass that is either a single wavelength or a list of tuples of the - form [(weight1, wavl1), (weight2, wavl2),...]. The model is + Generates the fringe model. + + Use the attributes of the object with a bandpass that is either a single + wavelength or a list of tuples of the form + [(weight1, wavl1), (weight2, wavl2),...]. The model is a collection of fringe intensities, where nholes = 7 means the model has a @D slice for each of 21 cosines, 21 sines, a DC-like, and a flux slice for a toal of 44 2D slices. @@ -294,8 +298,10 @@ def fit_image( weighted=False, ): """ - Run a least-squares fit on an input image; find the appropriate - wavelength scale and rotation. If a model is not specified then this + Run a least-squares fit on an input image. + + Find the appropriate wavelength scale and rotation. + If a model is not specified then this method will find the appropriate wavelength scale, rotation (and hopefully centering as well -- This is not written into the object yet, but should be soon). Without specifying a model, fit_image can take a @@ -419,7 +425,9 @@ def improve_scaling( self, img, scaleguess=None, rotstart=0.0, centering="PIXELCENTERED" ): """ - Determine the scale and rotation that best fits the data. Correlations + Determine the scale and rotation that best fits the data. + + Correlations are calculated in the image plane, in anticipation of data with many bad pixels. diff --git a/jwst/ami/mask_definition_ami.py b/jwst/ami/mask_definition_ami.py index 140dc539af..332c7abc46 100644 --- a/jwst/ami/mask_definition_ami.py +++ b/jwst/ami/mask_definition_ami.py @@ -1,21 +1,11 @@ -# -# Module for defining mask geometry in pupil space -# Replace old mask_definitions by reading info in nrm_model into a similar object. -# - import numpy as np import math from .utils import rotate2dccw -m = 1.0 -mm = 1.0e-3 * m -um = 1.0e-6 * m - - class NRMDefinition(): - def __init__(self, nrm_model, maskname='jwst_ami', rotdeg=None, chooseholes=None): + def __init__(self, nrm_model, maskname='jwst_ami', chooseholes=None): """ Set attributes of NRMDefinition class. @@ -28,8 +18,6 @@ def __init__(self, nrm_model, maskname='jwst_ami', rotdeg=None, chooseholes=None datamodel containing NRM reference file data maskname: string Identifier for mask geometry; default 'jwst_ami', optional - rotdeg: list of floats - range of rotations to search (degrees), optional chooseholes: list None, or e.g. ['B2', 'B4', 'B5', 'B6'] for a four-hole mask, optional If None, use real seven-hole mask @@ -46,9 +34,6 @@ def __init__(self, nrm_model, maskname='jwst_ami', rotdeg=None, chooseholes=None self.read_nrm_model(nrm_model, chooseholes=chooseholes) - if rotdeg is not None: - self.rotdeg = rotdeg - def read_nrm_model(self, nrm_model, chooseholes=None): """ Calculate hole centers with appropriate rotation. diff --git a/jwst/ami/nrm_core.py b/jwst/ami/nrm_core.py index e03c911895..055e37a205 100755 --- a/jwst/ami/nrm_core.py +++ b/jwst/ami/nrm_core.py @@ -1,8 +1,3 @@ -#! /usr/bin/env python - -# Module for fringe fitting class -# Original python was by A. Greenbaum & A. Sivaramakrishnan - import logging import numpy as np from . import lg_model @@ -18,8 +13,11 @@ class FringeFitter: def __init__(self, instrument_data, **kwargs): """ + Fit fringes to get interferometric observables for the data. + For the given information on the instrument and mask, calculate the fringe observables (visibilities and closure phases in the image plane. + Original python was by A. Greenbaum & A. Sivaramakrishnan Parameters ---------- @@ -160,8 +158,7 @@ def make_lgfitmodel(self): def fit_fringes_single_integration(self, slc): """ - Generate the best model to - match a single slice + Generate the best model to match a single slice. Parameters ---------- diff --git a/jwst/ami/oifits.py b/jwst/ami/oifits.py index b373babd24..52616089bc 100644 --- a/jwst/ami/oifits.py +++ b/jwst/ami/oifits.py @@ -1,12 +1,5 @@ #! /usr/bin/env python -""" -RawOifits class: takes fringefitter class, which contains nrm_list and instrument_data attributes, -all info needed to write oifits. -populate structure needed to write out oifits files according to schema. -averaged and multi-integration versions, sigma-clipped stats over ints - -CalibOifits class: takes two AmiOIModel datamodels and produces a final calibrated datamodel. -""" + import numpy as np from scipy.special import comb from astropy.stats import sigma_clipped_stats @@ -23,8 +16,14 @@ class RawOifits: def __init__(self, fringefitter, method="mean"): """ - Class to store AMI data in the format required to write out to OIFITS files - Angular quantities of input are in radians from fringe fitting; converted to degrees for saving. + Class to store AMI data in the format required to write out to OIFITS files. + + Takes fringefitter class, which contains nrm_list and instrument_data attributes, + all info needed to write oifits. Angular quantities of input are in radians from + fringe fitting; converted to degrees for saving. Populate the structure needed to + write out oifits files according to schema. + Produces averaged and multi-integration versions, with sigma-clipped stats over + integrations. Parameters ---------- @@ -729,6 +728,8 @@ def _format_STAINDEX_V2(self, tab): class CalibOifits: def __init__(self, targoimodel, caloimodel): """ + Produce a final calibrated AmiOIModel. + Calibrate (normalize) an AMI observation by subtracting closure phases of a reference star from those of a target and dividing visibility amplitudes of the target by those of the reference star.