Skip to content

Commit

Permalink
add initial support for having mask alignment defaults per mask
Browse files Browse the repository at this point in the history
  • Loading branch information
mperrin committed Dec 18, 2024
1 parent 0e0dcff commit cb6acbf
Show file tree
Hide file tree
Showing 2 changed files with 48 additions and 31 deletions.
12 changes: 12 additions & 0 deletions stpsf/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -407,3 +407,15 @@
'NIRSPEC': {'sigma': 0.05},
'MIRI': {'sigma': 0.05},
}

# Alignment information about instrument internal pupil masks (
INSTRUMENT_PUPIL_MASK_DEFAULT_POSITIONS = {
'NIRCam_MASKSWB': {'pupil_shift_x': None, 'pupil_shift_y': None, 'pupil_rotation': None},
'NIRCam_MASKLWB': {'pupil_shift_x': None, 'pupil_shift_y': None, 'pupil_rotation': None},
'NIRCam_MASKRND_SW': {'pupil_shift_x': None, 'pupil_shift_y': None, 'pupil_rotation': None},
'NIRCam_MASKRND_LW': {'pupil_shift_x': -0.012, 'pupil_shift_y': -0.023, 'pupil_rotation': -0.60}, # from K. Lawson, fits to ERS progid 1386 data
'MIRI_MASKFQPM_F1065C': {'pupil_shift_x': None, 'pupil_shift_y': None, 'pupil_rotation': None},
'MIRI_MASKFQPM_F11140': {'pupil_shift_x': None, 'pupil_shift_y': None, 'pupil_rotation': None},
'MIRI_MASKFQPM_F1550C': {'pupil_shift_x': None, 'pupil_shift_y': None, 'pupil_rotation': None},
'MIRI_MASKLYOT': {'pupil_shift_x': None, 'pupil_shift_y': None, 'pupil_rotation': None},
}
67 changes: 36 additions & 31 deletions stpsf/stpsf_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -1358,41 +1358,49 @@ def interpolate_was_opd(self, array, newdim):

return newopd

def _get_pupil_shift(self):
"""Return a tuple of pupil shifts, for passing to OpticalElement constructors
This is a minor utility function that gets used in most of the subclass optical
def _get_pupil_mask_alignment(self, lookup_key=None):
"""Return a tuple of pupil shifts and rotation, for passing to OpticalElement constructors
This is a utility function that gets used in most of the subclass optical
system construction.
This has two main parts:
1. Determine values for the pupil mask shift X, Y, and rotation, which can either be from:
1a) Explicitly provided by the user in self.options
1b) Or else, (optional) default positions per each mask, set in constants.py
1c) Otherwise return None
2. Convert any pupil mask shift X, Y from fractions of the pupil to offsets in meters
projected into the primary aperture.
For historical reasons, the pupil_shift_x and pupil_shift_y options are expressed
in fractions of the pupil. The parameters to poppy should now be expressed in
meters of shift. So the translation of that happens here.
Returns
-------
shift_x, shift_y : floats or Nones
Pupil shifts, expressed in meters.
shift_x, shift_y, rotation : floats or Nones
Pupil shifts, expressed in meters. And rotation in degrees.
"""
if ('pupil_shift_x' in self.options and self.options['pupil_shift_x'] != 0) or (
'pupil_shift_y' in self.options and self.options['pupil_shift_y'] != 0
):
from .constants import JWST_CIRCUMSCRIBED_DIAMETER

# missing values are treated as 0's
shift_x = self.options.get('pupil_shift_x', 0)
shift_y = self.options.get('pupil_shift_y', 0)
# nones are likewise treated as 0's
if shift_x is None:
shift_x = 0
if shift_y is None:
shift_y = 0
# Apply pupil scale
shift_x *= JWST_CIRCUMSCRIBED_DIAMETER
shift_y *= JWST_CIRCUMSCRIBED_DIAMETER
_log.info('Setting Lyot pupil shift to ({}, {})'.format(shift_x, shift_y))
else:
shift_x, shift_y = None, None
return shift_x, shift_y
if not lookup_key:
lookup_key = self.name + '_' + self.pupil_mask

values = []
for param in ('pupil_shift_x', 'pupil_shift_y', 'pupil_rotation'):
val = self.options.get(param) # has user directly provided a value?
# if not, check if we have a default for this instrument + mask
if (val is None) and (lookup_key in constants.INSTRUMENT_PUPIL_MASK_DEFAULT_POSITIONS):
val = constants.INSTRUMENT_PUPIL_MASK_DEFAULT_POSITIONS[lookup_key].get(param)
_log.debug(f' Found default {lookup_key} {param} = {val}')

if val is not None and param.startswith('pupil_shift'):
val *= constants.JWST_CIRCUMSCRIBED_DIAMETER
values.append(val)

shift_x, shift_y, rotation = values

if any(values):
_log.info(f"Setting instrument pupil mask shift to ({shift_x}, {shift_y}), rotation={rotation}")

return shift_x, shift_y, rotation

def _apply_jitter(self, result, local_options=None):
"""Modify a PSF to account for the blurring effects of image jitter.
Expand Down Expand Up @@ -2297,8 +2305,7 @@ def make_fqpm_wrapper(name, wavelength):
optsys.add_pupil(poppy.FQPM_FFT_aligner(direction='backward'))

# add pupil plane mask
shift_x, shift_y = self._get_pupil_shift()
rotation = self.options.get('pupil_rotation', None)
shift_x, shift_y, rotation = self._get_pupil_mask_alignment()

if self.options.get('coron_include_pre_lyot_plane', False) and self.pupil_mask.startswith('MASK'):
optsys.add_pupil(poppy.ScalarTransmission(name='Pre Lyot Stop'))
Expand Down Expand Up @@ -3009,8 +3016,7 @@ def _addAdditionalOptics(self, optsys, oversample=2):
trySAM = False

# add pupil plane mask
shift_x, shift_y = self._get_pupil_shift()
rotation = self.options.get('pupil_rotation', None)
shift_x, shift_y, rotation = self._get_pupil_mask_alignment()

if self.pupil_mask == 'CIRCLYOT' or self.pupil_mask == 'MASKRND':
optsys.add_pupil(
Expand Down Expand Up @@ -3537,8 +3543,7 @@ def _addAdditionalOptics(self, optsys, oversample=2):
radius = 0.0 # irrelevant but variable needs to be initialized

# add pupil plane mask
shift_x, shift_y = self._get_pupil_shift()
rotation = self.options.get('pupil_rotation', None)
shift_x, shift_y, rotation = self._get_pupil_mask_alignment()

# Note - the syntax for specifying shifts is different between FITS files and
# AnalyticOpticalElement instances. Annoying but historical.
Expand Down

0 comments on commit cb6acbf

Please sign in to comment.