Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update to read SI pixelscales directly from pysiaf #626

Merged
merged 2 commits into from
Jun 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 20 additions & 11 deletions webbpsf/tests/test_nircam.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,20 +192,26 @@ def test_nircam_get_detector():


def test_nircam_auto_pixelscale():
# This test now uses approximate equality in all the checks, to accomodate the fact that
# NIRCam pixel scales are drawn directly from SIAF for the aperture, and thus vary for each detector/
#
# 1.5% variance accomodates the differences between the various NRC detectors in each channel
close_enough = lambda a, b: np.isclose(a, b, rtol=0.015)

nc = webbpsf_core.NIRCam()

nc.filter='F200W'
assert nc.pixelscale == nc._pixelscale_short
assert close_enough(nc.pixelscale, nc._pixelscale_short)
assert nc.channel == 'short'

# auto switch to long
nc.filter='F444W'
assert nc.pixelscale == nc._pixelscale_long
assert close_enough(nc.pixelscale, nc._pixelscale_long)
assert nc.channel == 'long'

# and it can switch back to short:
nc.filter='F200W'
assert nc.pixelscale == nc._pixelscale_short
assert close_enough(nc.pixelscale, nc._pixelscale_short)
assert nc.channel == 'short'

nc.pixelscale = 0.0123 # user is allowed to set something custom
Expand All @@ -217,44 +223,47 @@ def test_nircam_auto_pixelscale():
nc.pixelscale = nc._pixelscale_long
# switch short again
nc.filter='F212N'
assert nc.pixelscale == nc._pixelscale_short
assert close_enough(nc.pixelscale, nc._pixelscale_short)
assert nc.channel == 'short'

# And test we can switch based on detector names too
nc.detector ='NRCA5'
assert nc.pixelscale == nc._pixelscale_long
assert close_enough(nc.pixelscale, nc._pixelscale_long)
assert nc.channel == 'long'

nc.detector ='NRCB1'
assert nc.pixelscale == nc._pixelscale_short
assert close_enough(nc.pixelscale, nc._pixelscale_short)
assert nc.channel == 'short'

nc.detector ='NRCA3'
assert nc.pixelscale == nc._pixelscale_short
assert close_enough(nc.pixelscale, nc._pixelscale_short)
assert nc.channel == 'short'


nc.auto_channel = False
# now we can switch filters and nothing else should change:
nc.filter='F480M'
assert nc.pixelscale == nc._pixelscale_short
assert close_enough(nc.pixelscale, nc._pixelscale_short)
assert nc.channel == 'short'

# but changing the detector explicitly always updates pixelscale, regardless
# of auto_channel being False

nc.detector = 'NRCA5'
assert nc.pixelscale == nc._pixelscale_long
assert close_enough(nc.pixelscale, nc._pixelscale_long)
assert nc.channel == 'long'


def test_validate_nircam_wavelengths():
# Same as above test: allow for up to 1.5% variance between NIRCam detectors in each channel
close_enough = lambda a, b: np.isclose(a, b, rtol=0.015)

nc = webbpsf_core.NIRCam()

# wavelengths fit on shortwave channel -> no exception
nc.filter='F200W'
nc._validate_config(wavelengths=np.linspace(nc.SHORT_WAVELENGTH_MIN, nc.SHORT_WAVELENGTH_MAX, 3))
assert nc.pixelscale == nc._pixelscale_short
assert close_enough(nc.pixelscale, nc._pixelscale_short)

# short wave is selected but user tries a long wave calculation
with pytest.raises(RuntimeError) as excinfo:
Expand All @@ -264,7 +273,7 @@ def test_validate_nircam_wavelengths():
# wavelengths fit on long channel -> no exception
nc.filter='F444W'
nc._validate_config(wavelengths=np.linspace(nc.LONG_WAVELENGTH_MIN, nc.LONG_WAVELENGTH_MAX, 3))
assert nc.pixelscale == nc._pixelscale_long
assert close_enough(nc.pixelscale, nc._pixelscale_long)

# long wave is selected but user tries a short wave calculation
with pytest.raises(RuntimeError) as excinfo:
Expand Down
71 changes: 53 additions & 18 deletions webbpsf/webbpsf_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -929,7 +929,9 @@ def _get_telescope_pupil_and_aberrations(self):

@SpaceTelescopeInstrument.aperturename.setter
def aperturename(self, value):
"""Set SIAF aperture name to new value, with validation
"""Set SIAF aperture name to new value, with validation.

This also updates the pixelscale to the local value for that aperture, for a small precision enhancement.
"""
# Explicitly update detector reference coordinates to the default for the new selected aperture,
# otherwise old coordinates can persist under certain circumstances
Expand All @@ -945,6 +947,13 @@ def aperturename(self, value):

# Only update if new value is different
if self._aperturename != value:
# First, check some info from current settings, wich we will use below as part of auto pixelscale code
# The point is to check if the pixel scale is set to a custom or default value,
# and if it's custom then don't override that.
# Note, check self._aperturename first to account for the edge case when this is called from __init__ before _aperturename is set
has_custom_pixelscale = self._aperturename and (self.pixelscale != self._get_pixelscale_from_apername(self._aperturename))

# Now apply changes:
self._aperturename = value
# Update detector reference coordinates
self.detector_position = (ap.XSciRef, ap.YSciRef)
Expand All @@ -953,6 +962,11 @@ def aperturename(self, value):
self._detector_geom_info = DetectorGeometry(self.siaf, self._aperturename)
_log.info(f"{self.name} SIAF aperture name updated to {self._aperturename}")

if not has_custom_pixelscale:
self.pixelscale = self._get_pixelscale_from_apername(self._aperturename)
_log.debug(f"Pixelscale updated to {self.pixelscale} based on average X+Y SciScale at SIAF aperture {self._aperturename}")


def _tel_coords(self):
""" Convert from science frame coordinates to telescope frame coordinates using
SIAF transformations. Returns (V2, V3) tuple, in arcminutes.
Expand Down Expand Up @@ -1003,6 +1017,13 @@ def set_position_from_aperture_name(self, aperture_name):
except KeyError:
raise ValueError("Not a valid aperture name for {}: {}".format(self.name, aperture_name))

def _get_pixelscale_from_apername(self, apername):
"""Simple utility function to look up pixelscale from apername"""
ap = self.siaf[apername]
# Here we make the simplifying assumption of **square** pixels, which is true within 0.5%.
# The slight departures from this are handled in the distortion model; see distortion.py
return (ap.XSciScale + ap.YSciScale) / 2

def _get_fits_header(self, result, options):
""" populate FITS Header keywords """
super(JWInstrument, self)._get_fits_header(result, options)
Expand Down Expand Up @@ -1628,8 +1649,8 @@ class MIRI(JWInstrument):
def __init__(self):
self.auto_pupil = True
JWInstrument.__init__(self, "MIRI")
self.pixelscale = 0.1108 # MIRI average of X and Y pixel scales. Source: SIAF PRDOPSSOC-031, 2021 April
self._rotation = 4.834 # V3IdlYAngle, Source: SIAF PRDOPSSOC-031
self.pixelscale = self._get_pixelscale_from_apername('MIRIM_FULL')
self._rotation = 4.83544897 # V3IdlYAngle, Source: SIAF PRDOPSSOC-059
# This is rotation counterclockwise; when summed with V3PA it will yield the Y axis PA on sky

self.options['pupil_shift_x'] = -0.0069 # CV3 on-orbit estimate (RPT028027) + OTIS delta from predicted (037134)
Expand Down Expand Up @@ -1899,27 +1920,25 @@ class NIRCam(JWInstrument):
LONG_WAVELENGTH_MAX = 5.3 * 1e-6

def __init__(self):
self._pixelscale_short = 0.0311 # average over both X and Y for short-wavelen channels, SIAF PRDOPSSOC-031, 2021 April
self._pixelscale_long = 0.0630 # average over both X and Y for long-wavelen channels, SIAF PRDOPSSOC-031, 2021 April
# need to set up a bunch of stuff here before calling superclass __init__
# so the overridden filter setter will not have errors when called from __init__
self.auto_channel = False
self.auto_aperturename = False
JWInstrument.__init__(self, "NIRCam")

self._pixelscale_short = self._get_pixelscale_from_apername('NRCA1_FULL')
self._pixelscale_long = self._get_pixelscale_from_apername('NRCA5_FULL')
self.pixelscale = self._pixelscale_short

self.options['pupil_shift_x'] = 0 # Set to 0 since NIRCam FAM corrects for PM shear in flight
self.options['pupil_shift_y'] = 0

# need to set up a bunch of stuff here before calling superclass __init__
# so the overridden filter setter will work successfully inside that.
# Enable the auto behaviours by default (after superclass __init__)
self.auto_channel = True
self.auto_aperturename = True
self._filter = 'F200W'
self._detector = 'NRCA1'

JWInstrument.__init__(self, "NIRCam") # do this after setting the long & short scales.
self._detector = 'NRCA1' # Must re-do this after superclass init since that sets it to None.
# This is an annoying workaround to ensure all the auto-channel stuff is ok

self.pixelscale = self._pixelscale_short # need to redo 'cause the __init__ call will reset it to zero
self._filter = 'F200W' # likewise need to redo

self.image_mask_list = ['MASKLWB', 'MASKSWB', 'MASK210R', 'MASK335R', 'MASK430R']
self._image_mask_apertures = {'MASKLWB': 'NRCA5_MASKLWB',
'MASKSWB': 'NRCA4_MASKSWB',
Expand Down Expand Up @@ -1998,6 +2017,10 @@ def _update_aperturename(self):

@JWInstrument.aperturename.setter
def aperturename(self, value):
"""Set SIAF aperture name to new value, with validation.

This also updates the pixelscale to the local value for that aperture, for a small precision enhancement.
"""
# Explicitly update detector reference coordinates,
# otherwise old coordinates can persist under certain circumstances

Expand Down Expand Up @@ -2027,6 +2050,13 @@ def aperturename(self, value):

# Only update if new value is different
if self._aperturename != value:
# First, check some info from current settings, wich we will use below as part of auto pixelscale code
# The point is to check if the pixel scale is set to a custom or default value,
# and if it's custom then don't override that.
# Note, check self._aperturename first to account for the edge case when this is called from __init__ before _aperturename is set
has_custom_pixelscale = self._aperturename and (self.pixelscale != self._get_pixelscale_from_apername(self._aperturename))

# Now apply changes:
self._aperturename = value
# Update detector reference coordinates
self.detector_position = (ap.XSciRef, ap.YSciRef)
Expand All @@ -2042,6 +2072,11 @@ def aperturename(self, value):
self._detector_geom_info = DetectorGeometry(self.siaf, self._aperturename)
_log.info("NIRCam aperture name updated to {}".format(self._aperturename))

if not has_custom_pixelscale:
self.pixelscale = self._get_pixelscale_from_apername(self._aperturename)
_log.debug(f"Pixelscale updated to {self.pixelscale} based on average X+Y SciScale at SIAF aperture {self._aperturename}")


@property
def module(self):
return self._detector[3]
Expand Down Expand Up @@ -2341,9 +2376,9 @@ class NIRSpec(JWInstrument):

def __init__(self):
JWInstrument.__init__(self, "NIRSpec")
self.pixelscale = 0.1043 # Average over both detectors. SIAF PRDOPSSOC-031, 2021 April
self.pixelscale = 0.10435 # Average over both detectors. SIAF PRDOPSSOC-059, 2022 Dec
# Microshutters are 0.2x0.46 but we ignore that here.
self._rotation = 138.4 # Average for both detectors in SIAF PRDOPSSOC-031
self._rotation = 138.5 # Average for both detectors in SIAF PRDOPSSOC-059
# This is rotation counterclockwise; when summed with V3PA it will yield the Y axis PA on sky
self.filter_list.append("IFU")
self._IFU_pixelscale = 0.1043 # same.
Expand Down Expand Up @@ -2476,7 +2511,7 @@ class NIRISS(JWInstrument):
def __init__(self, auto_pupil=True):
self.auto_pupil = auto_pupil
JWInstrument.__init__(self, "NIRISS")
self.pixelscale = 0.0656 # Average of X and Y scales, SIAF PRDOPSSOC-031, 2021 April
self.pixelscale = 0.065657 # Average of X and Y scales, SIAF PRDOPSSOC-059, 2022 Dec

self.options['pupil_shift_x'] = 0.0243 # CV3 on-orbit estimate (RPT028027) + OTIS delta from predicted (037134)
self.options['pupil_shift_y'] = -0.0141
Expand Down Expand Up @@ -2604,7 +2639,7 @@ class FGS(JWInstrument):

def __init__(self):
JWInstrument.__init__(self, "FGS")
self.pixelscale = 0.0691 # Average of X and Y scales for both detectors, SIAF PRDOPSSOC-031, 2021 April
self.pixelscale = 0.068991 # Average of X and Y scales for both detectors, SIAF PRDOPSSOC-059, 2022 Dec

self.options['pupil_shift_x'] = 0.0041 # CV3 on-orbit estimate (RPT028027) + OTIS delta from predicted (037134)
self.options['pupil_shift_y'] = -0.0023
Expand Down