diff --git a/.pep8speaks.yml b/.pep8speaks.yml
index 58f4683d..d46efe4c 100644
--- a/.pep8speaks.yml
+++ b/.pep8speaks.yml
@@ -12,7 +12,7 @@ message: # Customize the comment made by the bot
no_errors: "There are no PEP8 issues in this Pull Request."
scanner:
- diff_only: False # If True, errors caused by only the patch are shown
+ diff_only: True # If True, errors caused by only the patch are shown
pycodestyle:
max-line-length: 125 # Default is 79 in PEP8
diff --git a/dev_utils/compute_psf_library.py b/dev_utils/compute_psf_library.py
index cef8d274..dc4412ab 100644
--- a/dev_utils/compute_psf_library.py
+++ b/dev_utils/compute_psf_library.py
@@ -16,7 +16,7 @@
if not os.environ.get('WEBBPSF_PATH'):
os.environ['WEBBPSF_PATH'] = '/grp/jwst/ote/webbpsf-data'
-import webbpsf # noqa
+import webbpsf # noqa
N_PROCESSES = 16
diff --git a/dev_utils/field_dependence/basis.py b/dev_utils/field_dependence/basis.py
index 87e47cb0..2c12c2f6 100644
--- a/dev_utils/field_dependence/basis.py
+++ b/dev_utils/field_dependence/basis.py
@@ -22,20 +22,19 @@
os.environ['PINT_ARRAY_PROTOCOL_FALLBACK'] = '0'
-import pint # noqa
+import pint # noqa
units = pint.UnitRegistry()
Q_ = units.Quantity
# Silence NEP 18 warning
-import warnings # noqa
+import warnings # noqa
with warnings.catch_warnings():
warnings.simplefilter('ignore')
Q_([])
-
def embed(n, m):
# Return vector of indices needed to pull an array of length m from the center of an array of (larger) length n, or
# conversely insert an array of length m into an array of length n.
@@ -216,7 +215,7 @@ def numpolys(self):
def numpolys(self, new_numpolys):
polynomials_copy = self._polynomials
self._polynomials = np.zeros((new_numpolys, self._pts_x, self._pts_y))
- self._polynomials[0 : self._numpolys, :, :] = polynomials_copy
+ self._polynomials[0: self._numpolys, :, :] = polynomials_copy # noqa
self._numpolys = new_numpolys
def project(self, in_data):
@@ -480,7 +479,8 @@ def __init__(self, *args, **kwargs):
# next_order_start = (self._term_order[index] + 1) * (self._term_order[index] + 2) / 2
# radial_term_index = order_start + self._term_order[index] / 2
# if index < radial_term_index:
- # self._polynomial_map[index] = next_order_start - (index - order_start) * 2 - (self._term_order[index] - 1) / 2
+ # self._polynomial_map[index] =
+ # next_order_start - (index - order_start) * 2 - (self._term_order[index] - 1) / 2
# elif index > radial_term_index:
# self._polynomial_map[index] =
#
diff --git a/dev_utils/field_dependence/read_codev_dat_fits.py b/dev_utils/field_dependence/read_codev_dat_fits.py
index 0997c5bd..05eedc6f 100644
--- a/dev_utils/field_dependence/read_codev_dat_fits.py
+++ b/dev_utils/field_dependence/read_codev_dat_fits.py
@@ -45,12 +45,13 @@ def main():
]
# Zernike fitting order for each instrument
- order= {'fgs': 15,
- 'nircam': 15,
- 'miri': 15,
- 'nirspec': 15,
- 'niriss': 15
- }
+ order = {
+ 'fgs': 15,
+ 'nircam': 15,
+ 'miri': 15,
+ 'nirspec': 15,
+ 'niriss': 15
+ }
# Define the origin of the CodeV coordinate system in V2/V3 space
v2_origin_degrees = 0
diff --git a/dev_utils/wfe_benchmark.py b/dev_utils/wfe_benchmark.py
index e8a30aab..77ae65fe 100644
--- a/dev_utils/wfe_benchmark.py
+++ b/dev_utils/wfe_benchmark.py
@@ -9,11 +9,11 @@
inst = webbpsf.NIRCam()
inst.filter = "F430M"
-inst.detector_position = (1024,1024)
+inst.detector_position = (1024, 1024)
# Baseline test: No SI WFE, no distortion
inst.include_si_wfe = False
-%timeit psf = inst.calc_psf(add_distortion=False, monochromatic=4.3e-6) # noqa
+%timeit psf = inst.calc_psf(add_distortion=False, monochromatic=4.3e-6) # noqa
# Result: 911 ms ± 5.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit psf = inst.calc_psf(add_distortion=False, nlambda=ncores) # noqa
# Result: 5.62 s ± 177 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
@@ -26,8 +26,8 @@
# Result: 6.1 s ± 96.9 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
# Use pixel (0,0) to force extrapolation algorithm
-inst.detector_position = (0,0)
-%timeit psf = inst.calc_psf(add_distortion=False, monochromatic=4.3e-6) # noqa
+inst.detector_position = (0, 0)
+%timeit psf = inst.calc_psf(add_distortion=False, monochromatic=4.3e-6) # noqa
# Result: 1.8 s ± 12.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
-%timeit psf = inst.calc_psf(add_distortion=False, nlambda=ncores) # noqa
+%timeit psf = inst.calc_psf(add_distortion=False, nlambda=ncores) # noqa
# Result: 6.53 s ± 85.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
diff --git a/docs/conf.py b/docs/conf.py
index ff1ff2d8..3eed6ead 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -118,7 +118,7 @@
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = "sphinx"
-intersphinx_mapping.update( # noqa - defined in star import
+intersphinx_mapping.update( # noqa - defined in star import
{
"poppy": ("http://poppy-optics.readthedocs.io/", None),
}
diff --git a/docs/exts/numfig.py b/docs/exts/numfig.py
index e9d63e27..b585fb19 100644
--- a/docs/exts/numfig.py
+++ b/docs/exts/numfig.py
@@ -1,11 +1,12 @@
from docutils.nodes import SkipNode, Text, caption, figure, raw, reference
from sphinx.roles import XRefRole
-# Element classes
+# Element classes
class page_ref(reference):
pass
+
class num_ref(reference):
pass
@@ -15,10 +16,12 @@ class num_ref(reference):
def skip_page_ref(self, node):
raise SkipNode
+
def latex_visit_page_ref(self, node):
self.body.append("\\pageref{%s:%s}" % (node['refdoc'], node['reftarget']))
raise SkipNode
+
def latex_visit_num_ref(self, node):
fields = node['reftarget'].split('#')
if len(fields) > 1:
@@ -57,7 +60,6 @@ def doctree_resolved(app, doctree, docname):
i += 1
-
# replace numfig nodes with links
if app.builder.name != 'latex':
for ref_info in doctree.traverse(num_ref):
@@ -75,16 +77,17 @@ def doctree_resolved(app, doctree, docname):
target_doc = app.builder.env.figid_docname_map[target]
link = "%s#%s" % (app.builder.get_relative_uri(docname, target_doc),
target)
- html = '' % (link, labelfmt %(figids[target]))
+ html = '' % (link, labelfmt % (figids[target]))
ref_info.replace_self(raw(html, html, format='html'))
else:
ref_info.replace_self(Text(labelfmt % (figids[target])))
def clean_env(app):
- app.builder.env.i=1
+ app.builder.env.i = 1
app.builder.env.figid_docname_map = {}
+
def setup(app):
app.add_config_value('number_figures', True, True)
app.add_config_value('figure_caption_prefix', "Figure", True)
diff --git a/webbpsf/constants.py b/webbpsf/constants.py
index 27785ba5..4222b398 100644
--- a/webbpsf/constants.py
+++ b/webbpsf/constants.py
@@ -400,4 +400,4 @@
# See Gaspar et al. 2021 for illustrative figures.
# This is a rough approximation of a detector-position-dependent phenomenon
MIRI_CRUCIFORM_INNER_RADIUS_PIX = 12
-MIRI_CRUCIFORM_RADIAL_SCALEFACTOR = 0.005 # Brightness factor for the diffuse circular halo
\ No newline at end of file
+MIRI_CRUCIFORM_RADIAL_SCALEFACTOR = 0.005 # Brightness factor for the diffuse circular halo
diff --git a/webbpsf/detectors.py b/webbpsf/detectors.py
index fe4aa86e..a3a1e973 100644
--- a/webbpsf/detectors.py
+++ b/webbpsf/detectors.py
@@ -1,15 +1,16 @@
import copy
-
import os
+
+import astropy.convolution
import numpy as np
import scipy
-import webbpsf
-from webbpsf import utils, constants
-from astropy.convolution.kernels import CustomKernel
+import scipy.signal as signal
from astropy.convolution import convolve
+from astropy.convolution.kernels import CustomKernel
from astropy.io import fits
-import astropy.convolution
-import scipy.signal as signal
+
+import webbpsf
+from webbpsf import constants, utils
def get_detector_ipc_model(inst, header):
@@ -60,7 +61,8 @@ def get_detector_ipc_model(inst, header):
# PPC effect
# read the SCA extension for the detector
- ## TODO: This depends on detector coordinates, and which readout amplifier. if in subarray, then the PPC effect is always like in amplifier 1
+ # TODO: This depends on detector coordinates, and which readout amplifier.
+ # If in subarray, then the PPC effect is always like in amplifier 1
sca_path_ppc = os.path.join(utils.get_webbpsf_data_path(), 'NIRCam', 'IPC', 'KERNEL_PPC_CUBE.fits')
kernel_ppc = CustomKernel(fits.open(sca_path_ppc)[det2sca[det]].data[0]) # we read the first slice in the cube
@@ -167,7 +169,8 @@ def apply_detector_ipc(psf_hdulist, extname='DET_DIST'):
"""
- # In cases for which the user has asked for the IPC to be applied to a not-present extension, we have nothing to add this to
+ # In cases for which the user has asked for the IPC
+ # to be applied to a not-present extension, we have nothing to add this to
if extname not in psf_hdulist:
webbpsf.webbpsf_core._log.debug(f'Skipping IPC simulation since ext {extname} is not found')
return
@@ -283,9 +286,11 @@ def oversample_ipc_model(kernel, oversample):
# Functions for applying MIRI Detector Scattering Effect
-# Lookup tables of shifts of the cruciform, estimated roughly from F560W ePSFs (ePSFs by Libralatto, shift estimate by Perrin)
-cruciform_xshifts = scipy.interpolate.interp1d([0, 357, 1031], [1.5,0.5,-0.9], kind='linear', fill_value='extrapolate')
-cruciform_yshifts = scipy.interpolate.interp1d([0, 511, 1031], [1.6,0,-1.6], kind='linear', fill_value='extrapolate')
+# Lookup tables of shifts of the cruciform
+# Estimated roughly from F560W ePSFs (ePSFs by Libralatto, shift estimate by Perrin)
+cruciform_xshifts = scipy.interpolate.interp1d([0, 357, 1031], [1.5, 0.5, -0.9], kind='linear', fill_value='extrapolate')
+cruciform_yshifts = scipy.interpolate.interp1d([0, 511, 1031], [1.6, 0, -1.6], kind='linear', fill_value='extrapolate')
+
def _make_miri_scattering_kernel_2d(in_psf, kernel_amp, oversample=1, wavelength=5.5, detector_position=(0, 0)):
"""Improved / more complex model of the MIRI cruciform, with parameterization to model
@@ -310,13 +315,13 @@ def _make_miri_scattering_kernel_2d(in_psf, kernel_amp, oversample=1, wavelength
"""
# make output array
npix = in_psf.shape[0]
- cen = (npix-1) // 2
- kernel_2d = np.zeros( (npix, npix), float)
+ cen = (npix - 1) // 2
+ kernel_2d = np.zeros((npix, npix), float)
- ### make 1d kernels for the main cruciform bright lines
+ # make 1d kernels for the main cruciform bright lines
# Compute 1d indices
x = np.arange(npix, dtype=float)
- x -= (npix-1)/2
+ x -= (npix - 1) / 2
x /= oversample
y = x # we're working in 1d in this part, but clarify let's have separate coords for each axis
@@ -330,19 +335,22 @@ def _make_miri_scattering_kernel_2d(in_psf, kernel_amp, oversample=1, wavelength
# Add in the offset copies of the main 1d kernels
# Empirically, the 'center' of the cruciform shifts inwards towards the center of the detector
- # i.e. for the upper right corner, the cruciform shifts down and left a bit, etc.
+ # i.e. for the upper right corner, the cruciform shifts down and left a bit, etc.
yshift = cruciform_yshifts(detector_position[1])
xshift = cruciform_xshifts(detector_position[0])
- kernel_2d[cen + int(round(yshift*oversample))] = kernel_x
- kernel_2d[:, cen + int(round(xshift*oversample))] = kernel_y
+ kernel_2d[cen + int(round(yshift * oversample))] = kernel_x
+ kernel_2d[:, cen + int(round(xshift * oversample))] = kernel_y
- ### create and add in the more diffuse radial term
+ # create and add in the more diffuse radial term
# Model this as an expoential falloff outside the inner radius, times some scale factor relative to the above
y, x = np.indices(kernel_2d.shape)
- r = np.sqrt((x-cen)**2 + (y-cen)**2) / oversample
- radial_term = np.exp(-r/2/webbpsf.constants.MIRI_CRUCIFORM_INNER_RADIUS_PIX) * kernel_amp \
- * (r > webbpsf.constants.MIRI_CRUCIFORM_INNER_RADIUS_PIX) \
- * webbpsf.constants.MIRI_CRUCIFORM_RADIAL_SCALEFACTOR
+ r = np.sqrt((x - cen) ** 2 + (y - cen) ** 2) / oversample
+ radial_term = (
+ np.exp(-r / 2 / webbpsf.constants.MIRI_CRUCIFORM_INNER_RADIUS_PIX)
+ * kernel_amp
+ * (r > webbpsf.constants.MIRI_CRUCIFORM_INNER_RADIUS_PIX)
+ * webbpsf.constants.MIRI_CRUCIFORM_RADIAL_SCALEFACTOR
+ )
kernel_2d += radial_term
@@ -373,9 +381,9 @@ def _apply_miri_scattering_kernel_2d(in_psf, kernel_2d, oversample):
"""
# Convolve the input PSF with the kernel for scattering
- im_conv = astropy.convolution.convolve_fft(in_psf, kernel_2d, boundary='fill', fill_value=0.0,
- normalize_kernel=False, nan_treatment='fill', allow_huge = True)
-
+ im_conv = astropy.convolution.convolve_fft(
+ in_psf, kernel_2d, boundary='fill', fill_value=0.0, normalize_kernel=False, nan_treatment='fill', allow_huge=True
+ )
# Normalize.
# Note, it appears we do need to correct the amplitude for the sampling factor. Might as well do that here.
@@ -489,9 +497,13 @@ def apply_miri_scattering(hdulist_or_filename=None, kernel_amp=None, old_method=
in_psf = psf[ext].data
# create cruciform model using improved method using a 2d convolution kernel, attempting to model more physics.
- kernel_2d = _make_miri_scattering_kernel_2d(in_psf, kernel_amp, oversample,
- detector_position= (hdu_list[0].header['DET_X'], hdu_list[0].header['DET_Y']),
- wavelength = hdu_list[0].header['WAVELEN']*1e6 )
+ kernel_2d = _make_miri_scattering_kernel_2d(
+ in_psf,
+ kernel_amp,
+ oversample,
+ detector_position=(hdu_list[0].header['DET_X'], hdu_list[0].header['DET_Y']),
+ wavelength=hdu_list[0].header['WAVELEN'] * 1e6,
+ )
im_conv_both = _apply_miri_scattering_kernel_2d(in_psf, kernel_2d, oversample)
# Add this 2D scattered light output to the PSF
@@ -511,25 +523,22 @@ def apply_miri_scattering(hdulist_or_filename=None, kernel_amp=None, old_method=
return psf
-def _show_miri_cruciform_kernel(filt, npix=101, oversample=4, detector_position=(512,512), ax=None):
- """ utility function for viewing/visualizing the cruciform kernel
- """
+def _show_miri_cruciform_kernel(filt, npix=101, oversample=4, detector_position=(512, 512), ax=None):
+ """utility function for viewing/visualizing the cruciform kernel"""
import matplotlib
- placeholder = np.zeros((npix*oversample, npix*oversample))
+ placeholder = np.zeros((npix * oversample, npix * oversample))
kernel_amp = get_miri_cruciform_amplitude(filt)
- extent =[-npix/2, npix/2, -npix/2, npix/2]
+ extent = [-npix / 2, npix / 2, -npix / 2, npix / 2]
- kernel_2d = _make_miri_scattering_kernel_2d(placeholder, kernel_amp, oversample,
- detector_position= detector_position)
+ kernel_2d = _make_miri_scattering_kernel_2d(placeholder, kernel_amp, oversample, detector_position=detector_position)
norm = matplotlib.colors.LogNorm(1e-6, 1)
cmap = matplotlib.cm.viridis
cmap.set_bad(cmap(0))
if ax is None:
ax = matplotlib.pyplot.gca()
ax.imshow(kernel_2d, norm=norm, cmap=cmap, extent=extent, origin='lower')
- ax.set_title(f"MIRI cruciform model for {filt}, position {detector_position}, oversample {oversample}")
- ax.plot(0,0,marker='+', color='yellow')
+ ax.set_title(f'MIRI cruciform model for {filt}, position {detector_position}, oversample {oversample}')
+ ax.plot(0, 0, marker='+', color='yellow')
matplotlib.pyplot.colorbar(mappable=ax.images[0])
-
diff --git a/webbpsf/gridded_library.py b/webbpsf/gridded_library.py
index 35cf6788..10f8d1e0 100644
--- a/webbpsf/gridded_library.py
+++ b/webbpsf/gridded_library.py
@@ -579,7 +579,6 @@ def tuple_to_int(t):
if isinstance(t, tuple):
return (int(t[0]), int(t[1]))
-
def show_grid_helper(grid, data, title='Grid of PSFs', vmax=0, vmin=0, scale='log'):
npsfs = grid.data.shape[0]
n = int(np.sqrt(npsfs))
diff --git a/webbpsf/mast_wss.py b/webbpsf/mast_wss.py
index 0c93677d..7ac1e992 100644
--- a/webbpsf/mast_wss.py
+++ b/webbpsf/mast_wss.py
@@ -14,7 +14,7 @@
import webbpsf.utils
-### Login and authentication
+# Login and authentication
service = 'Mast.Jwst.Filtered.Wss'
@@ -54,7 +54,7 @@ def download_all_opds(opdtable, verbose=False):
webbpsf.mast_wss.mast_retrieve_opd(row['fileName'], verbose=verbose)
-### Functions for searching and retrieving OPDs based on time
+# Functions for searching and retrieving OPDs based on time
def mast_wss_date_query(date, tdelta):
@@ -103,13 +103,16 @@ def mast_wss_opds_around_date_query(date, verbose=True):
if tdelta >= 6 * u.day:
if verbose:
print(
- 'Could not find JWST OPDs both before and after the specified date. Date outside of the available range of WFS data.'
+ 'Could not find JWST OPDs both before and after the specified date.',
+ 'Date outside of the available range of WFS data.'
)
if len(obs_table) == 0:
- raise RuntimeError(
- 'Cannot find ANY OPDs in MAST within a week before/after that date. Date is likely outside the range of valid data.'
+ error_message = (
+ 'Cannot find ANY OPDs in MAST within a week before/after that date.'
+ 'Date is likely outside the range of valid data.'
)
+ raise RuntimeError(error_message)
elif max(obs_table['date_obs_mjd']) < date.mjd:
# if len(obs_table) == 1 : #and min(obs_table['date_obs_mjd']) < date.mjd:
if verbose:
@@ -222,7 +225,7 @@ def get_opd_at_time(date, choice='closest', verbose=False, output_path=None):
elif choice == 'average':
if verbose:
print(f'User requested calculating OPD time averaged around {date}')
- mast_retrieve_opd(pre_opd_fn, output_path=output_path) # TODO - define pre_opd_fn now or skip?
+ mast_retrieve_opd(prev_opd_fn, output_path=output_path)
mast_retrieve_opd(post_opd_fn, output_path=output_path)
raise NotImplementedError('Not yet implemented')
elif choice == 'closest':
@@ -231,12 +234,13 @@ def get_opd_at_time(date, choice='closest', verbose=False, output_path=None):
)
if verbose:
print(
- f'User requested choosing OPD time closest in time to {date}, which is {closest_fn}, delta time {closest_dt:.3f} days'
+ f'User requested choosing OPD time closest in time to {date},',
+ f'which is {closest_fn}, delta time {closest_dt:.3f} days'
)
return mast_retrieve_opd(closest_fn, output_path=output_path)
-### Functions for format conversion of OPDs
+# Functions for format conversion of OPDs
def import_wss_opd(filename, npix_out=1024, verbose=False):
@@ -310,7 +314,7 @@ def import_wss_opd(filename, npix_out=1024, verbose=False):
return wasopd
-## Functions for dealing with time series or entire set of OPDs
+# Functions for dealing with time series or entire set of OPDs
def infer_pre_or_post_correction(row):
@@ -503,7 +507,8 @@ def deduplicate_opd_table(opdtable, drop_f187n=True, verbose=False):
if opdtable[row_index]['corr_id'] in invalid_aps:
if verbose:
print(
- f"{opdtable[row_index]['fileName']} is flaggd as an invalid AP due to known analysis issue(s). Ignoring it."
+ f"{opdtable[row_index]['fileName']}",
+ "is flaggd as an invalid AP due to known analysis issue(s). Ignoring it."
)
continue
elif opdtable[row_index]['date'] in measurement_dates_encountered:
@@ -701,7 +706,8 @@ def download_wfsc_images(program=None, obs=None, verbose=False, **kwargs):
if verbose:
date_obs = astropy.time.Time(filetable[0]['date_obs_mjd'], format='mjd')
print(
- f"Found {len(filetable)} level 3 data products from {filetable[0]['program']}:{filetable[0]['observtn']} around {date_obs.iso[0:16]}"
+ f"Found {len(filetable)} level 3 data products from",
+ f"{filetable[0]['program']}:{filetable[0]['observtn']} around {date_obs.iso[0:16]}"
)
if len(filetable) > 0:
file_list = []
diff --git a/webbpsf/match_data.py b/webbpsf/match_data.py
index 578eb2bb..8b8e372a 100644
--- a/webbpsf/match_data.py
+++ b/webbpsf/match_data.py
@@ -1,4 +1,4 @@
-## Functions to match or fit PSFs to observed JWST data
+# Functions to match or fit PSFs to observed JWST data
import astropy
import astropy.io.fits as fits
import pysiaf
@@ -156,7 +156,7 @@ def get_nrc_coron_apname(input):
# Find all instances of "_"
inds = [pos for pos, char in enumerate(apname_pps) if char == '_']
# Filter is always appended to end, but can have different string sizes (F322W2)
- filter = apname_pps[inds[-1] + 1 :]
+ filter = apname_pps[inds[-1] + 1:]
apname_new += f'_{filter}'
elif last_str == 'NARROW':
apname_new += '_NARROW'
diff --git a/webbpsf/opds.py b/webbpsf/opds.py
index 7313c522..fbc77fad 100644
--- a/webbpsf/opds.py
+++ b/webbpsf/opds.py
@@ -152,9 +152,11 @@ def __init__(
self._segment_masks = fits.getdata(full_seg_mask_file)
if not self._segment_masks.shape[0] == self.npix:
- raise ValueError(
- f'The shape of the segment mask file {self._segment_masks.shape} does not match the shape expect: ({self.npix}, {self.npix})'
+ error_message = (
+ f'The shape of the segment mask file {self._segment_masks.shape} '
+ 'does not match the shape expect: ({self.npix}, {self.npix})'
)
+ raise ValueError(error_message)
self._segment_masks_version = fits.getheader(full_seg_mask_file)['VERSION']
@@ -246,7 +248,6 @@ def writeto(self, outname, overwrite=True, **kwargs):
"""Write OPD to a FITS file on disk"""
self.as_fits(**kwargs).writeto(outname, overwrite=overwrite)
-
def display_opd(
self,
ax=None,
@@ -849,9 +850,10 @@ def _move(self, segment, type='tilt', vector=None, display=False, return_zernike
# local_vector = np.array([local_coordX, local_coordY, vector[2]])
local_vector = vector
if type == 'tilt':
+ # convert Z tilt to milliradians instead of microradians because that is what the sensitivity tables use
local_vector[
2
- ] /= 1000 # convert Z tilt to milliradians instead of microradians because that is what the sensitivity tables use
+ ] /= 1000
units = 'microradians for tip/tilt, milliradians for clocking'
else:
units = 'microns'
@@ -1133,7 +1135,8 @@ def __init__(
Note, if OPD is None, then this will be ignored and the nominal field dependence will be disabled.
control_point_fieldpoint: str
A parameter used in the field dependence model for a misaligned secondary mirror.
- Name of the field point where the OTE MIMF control point is located, on instrument defined by "control_point_instr".
+ Name of the field point where the OTE MIMF control point is located,
+ on instrument defined by "control_point_instr".
Default: 'nrca3_full'.
The OTE control point is the field point to which the OTE has been aligned and defines the field angles
for the field-dependent SM pose aberrations.
@@ -1166,7 +1169,8 @@ def __init__(
for icol in cnames[3:]:
self._influence_fns[icol] *= -1
- # WFTP10 hotfix for RoC sign inconsistency relative to everything else, due to outdated version of WAS IFM used in table construction.
+ # WFTP10 hotfix for RoC sign inconsistency relative to everything else,
+ # due to outdated version of WAS IFM used in table construction.
# FIXME update the IFM file on disk and then delete the next three lines
roc_rows = self._influence_fns['control_mode'] == 'ROC'
for icol in self._influence_fns.colnames[3:]:
@@ -1746,9 +1750,13 @@ def _get_zernikes_for_ote_field_dep(
clip_dist = np.sqrt((x_field_pt - x_field_pt0) ** 2 + (y_field_pt - y_field_pt0) ** 2)
if clip_dist > 0.1 * u.arcsec:
# warn the user we're making an adjustment here (but no need to do so if the distance is trivially small)
- warnings.warn(
- f'For (V2,V3) = {v2v3}, Field point {x_field_pt}, {y_field_pt} not within valid region for field dependence model of OTE WFE for {instrument}: {min_x_field}-{max_x_field}, {min_y_field}-{max_y_field}. Clipping to closest available valid location, {clip_dist} away from the requested coordinates.'
+ warning_message = (
+ f'For (V2,V3) = {v2v3}, Field point {x_field_pt}, {y_field_pt} '
+ 'not within valid region for field dependence model of OTE WFE for '
+ f'{instrument}: {min_x_field}-{max_x_field}, {min_y_field}-{max_y_field}. '
+ f'Clipping to closest available valid location, {clip_dist} away from the requested coordinates.'
)
+ warnings.warn(warning_message)
# Get value of Legendre Polynomials at desired field point. Need to implement model in G. Brady's prototype
# polynomial basis code, independent of that code for now. Perhaps at some point in the future this model
@@ -2879,12 +2887,8 @@ def random_unstack(ote, radius=1, verbose=False):
ote.update_opd(verbose=verbose)
-
-
# -------------------------------------------------------------------------------
# Thermal
-
-
class OteThermalModel(object):
"""
Create an object for a delta_time that predicts the WSS Hexike coefficients
@@ -3288,9 +3292,12 @@ def sur_to_opd(sur_filename, ignore_missing=False, npix=256):
if not os.path.exists(sur_filename):
if not ignore_missing:
- raise FileNotFoundError(
- f'Missing SUR: {sur_filename}. Download of these should eventually be automated; for now, manually retrieve from WSSTAS at https://wsstas.stsci.edu/wsstas/staticPage/showContent/RecentSURs?primary=master.png'
+ error_message = (
+ f'Missing SUR: {sur_filename}. Download of these should eventually be automated; '
+ 'for now, manually retrieve from WSSTAS at '
+ 'https://wsstas.stsci.edu/wsstas/staticPage/showContent/RecentSURs?primary=master.png'
)
+ raise FileNotFoundError(error_message)
else:
return np.zeros((npix, npix), float)
ote.move_sur(sur_filename)
diff --git a/webbpsf/optical_budget.py b/webbpsf/optical_budget.py
index 2ac23a15..12bb2c71 100644
--- a/webbpsf/optical_budget.py
+++ b/webbpsf/optical_budget.py
@@ -9,7 +9,7 @@
import webbpsf
from webbpsf.utils import rms
-### JWST Optical Budgets Information
+# JWST Optical Budgets Information
# This module makes extensive use of information from the JWST Optical Budget
# by Paul Lightsey et al.
# See Lightsey's 'Guide to the Optical Budget'
diff --git a/webbpsf/optics.py b/webbpsf/optics.py
index bb1f9af6..ed2e2d18 100644
--- a/webbpsf/optics.py
+++ b/webbpsf/optics.py
@@ -17,8 +17,7 @@
_log = logging.getLogger('webbpsf')
-
-####### Classes for modeling aspects of JWST's segmented active primary #####
+# Classes for modeling aspects of JWST's segmented active primary #####
def segment_zernike_basis(segnum=1, nterms=15, npix=512, outside=np.nan):
@@ -187,7 +186,7 @@ def __init__(self, instrument=None, level='requirements', opd_index=0, **kwargs)
# TODO apply that to as a modification to the OPD array.
-####### Custom Optics used in JWInstrument classes #####
+# Custom Optics used in JWInstrument classes #####
class NIRSpec_three_MSA_shutters(poppy.AnalyticOpticalElement):
@@ -386,7 +385,9 @@ def __init__(
if which == 'LLNL':
raise NotImplementedError('Rotated field mask for LLNL grism not yet implemented!')
elif which == 'Bach':
- transmission = os.path.join(utils.get_webbpsf_data_path(), 'NIRISS/optics/MASKGR700XD.fits.gz') # TODO - Unused value, delete entire statement?
+ transmission = os.path.join(
+ utils.get_webbpsf_data_path(), 'NIRISS/optics/MASKGR700XD.fits.gz'
+ ) # TODO - Unused value, delete entire statement?
else:
raise NotImplementedError('Unknown grating name:' + which)
@@ -441,7 +442,8 @@ def __init__(
# self.pupil_demagnification = 173.56 # meters on the primary / meters in the NIRISS pupil
# Anand says:
- # nominally the circumscribing circle at the PW of NIRISS is ~40mm. I use 39mm for the nrm, but it's slightly field-dependent. Compare that to the 6.6... PM circle?
+ # nominally the circumscribing circle at the PW of NIRISS is ~40mm.
+ # I use 39mm for the nrm, but it's slightly field-dependent. Compare that to the 6.6... PM circle?
self.pupil_demagnification = 6.6 / 0.040 # about 165
# perform an initial population of the OPD array for display etc.
@@ -511,8 +513,8 @@ def get_opd(self, wave):
# now compute the spatially dependent sag of the cylinder, as projected onto the primary
# what is the pupil scale at the *reimaged pupil* of the grism?
- pupil_scale_m_per_pix = 38.0255e-6 # Based on UdeM info in wfe_cylindricallens.pro # TODO - unused, can be deleted or just commented out?
- # sag = np.sqrt(self.cylinder_radius**2 - (x*self.amplitude_header['PUPLSCAL']/self.pupil_demagnification)**2) - self.cylinder_radius
+ pupil_scale_m_per_pix = 38.0255e-6 # Based on UdeM info in wfe_cylindricallens.pro # noqa TODO - unused, can be deleted or just commented out?
+ # sag = np.sqrt(self.cylinder_radius**2 - (x*self.amplitude_header['PUPLSCAL']/self.pupil_demagnification)**2) - self.cylinder_radius # noqa
sag = np.sqrt(self.cylinder_radius**2 - (x / self.pupil_demagnification) ** 2) - self.cylinder_radius
# sag = self.cylinder_radius - np.sqrt(self.cylinder_radius**2 - (x * pupil_scale_m_per_pix )**2 )
@@ -547,7 +549,7 @@ def get_opd(self, wave):
def get_transmission(self, wave):
"""Make array for the pupil obscuration appropriate to the grism"""
- if isinstance(wave, poppy.Wavefront): # TODO - Wavelength isn't used, safe to delete in this function?
+ if isinstance(wave, poppy.Wavefront): # TODO - Wavelength isn't used, safe to delete in this function?
wavelength = wave.wavelength
else:
wave = poppy.Wavefront(wavelength=float(wave))
@@ -908,53 +910,31 @@ def get_transmission(self, wave):
# coronagraph regions
# Note: 180 deg rotation needed relative to Krist's figures for the flight SCI orientation:
- if ((self.module == 'A' and self.name == 'MASKLWB') or
- (self.module == 'B' and self.name == 'MASK210R')):
+ if (self.module == 'A' and self.name == 'MASKLWB') or (self.module == 'B' and self.name == 'MASK210R'):
# left edge:
# has one fully in the corner and one half in the other corner, half outside the 10x10 box
- wnd_5 = np.where(
- ((y < -5) & (y > -10)) &
- (
- ((x > 5) & (x < 10)) |
- ((x < -7.5) & (x > -12.5))
- )
- )
- wnd_2 = np.where(
- ((y < 10) & (y > 8)) &
- (
- ((x > 8) & (x < 10)) |
- ((x < -9) & (x > -11))
- )
- )
- elif ((self.module == 'A' and self.name == 'MASK210R') or
- (self.module == 'B' and self.name == 'MASKSWB')):
+ wnd_5 = np.where(((y < -5) & (y > -10))
+ & (((x > 5) & (x < 10))
+ | ((x < -7.5) & (x > -12.5))))
+ wnd_2 = np.where(((y < 10) & (y > 8))
+ & (((x > 8) & (x < 10))
+ | ((x < -9) & (x > -11))))
+ elif (self.module == 'A' and self.name == 'MASK210R') or (self.module == 'B' and self.name == 'MASKSWB'):
# right edge
- wnd_5 = np.where(
- ((y < -5) & (y > -10)) &
- (
- ((x < 12.5) & (x > 7.5)) |
- ((x < -5) & (x > -10))
- )
- )
- wnd_2 = np.where(
- ((y < 10) & (y > 8)) &
- (
- ((x < 11) & (x > 9)) |
- ((x < -8) & (x > -10))
- )
- )
+ wnd_5 = np.where(((y < -5) & (y > -10))
+ & (((x < 12.5) & (x > 7.5))
+ | ((x < -5) & (x > -10))))
+ wnd_2 = np.where(((y < 10) & (y > 8))
+ & (((x < 11) & (x > 9))
+ | ((x < -8) & (x > -10))))
else:
# the others have two, one in each corner, both halfway out of the 10x10 box.
- wnd_5 = np.where(
- ((y < -5) & (y > -10)) &
- (np.abs(x) > 7.5) &
- (np.abs(x) < 12.5)
- )
- wnd_2 = np.where(
- ((y < 10) & (y > 8)) &
- (np.abs(x) > 9) &
- (np.abs(x) < 11)
- )
+ wnd_5 = np.where(((y < -5) & (y > -10))
+ & (np.abs(x) > 7.5)
+ & (np.abs(x) < 12.5))
+ wnd_2 = np.where(((y < 10) & (y > 8))
+ & (np.abs(x) > 9)
+ & (np.abs(x) < 11))
self.transmission[wnd_5] = np.sqrt(1e-3)
self.transmission[wnd_2] = np.sqrt(1e-3)
@@ -1426,7 +1406,7 @@ def __init__(self, instrument, include_oversize=False, **kwargs):
)
# cut out central region to match the OPD, which is hard coded
# to 1024
- self.amplitude = self.amplitude[256 : 256 + 1024, 256 : 256 + 1024]
+ self.amplitude = self.amplitude[256: 256 + 1024, 256: 256 + 1024]
elif self.instrument.name == 'MIRI':
self.amplitude = fits.getdata(
os.path.join(
@@ -2029,7 +2009,7 @@ def __init__(
self.ztable_full = None
- ## REFERENCE:
+ # REFERENCE:
# NIRCam weak lenses, values from WSS config file, PRDOPSFLT-027
# A B
# WLP4_diversity = 8.27309 8.3443 diversity in microns
diff --git a/webbpsf/roman.py b/webbpsf/roman.py
index 8e1ecaaf..93ebe0f9 100644
--- a/webbpsf/roman.py
+++ b/webbpsf/roman.py
@@ -312,9 +312,11 @@ def calc_psf(
if self.name == 'RomanCoronagraph' and add_distortion is True:
self.options['add_distortion'] = False
self.options['crop_psf'] = False
- _log.info(
- 'Geometric distortions are not implemented in WebbPSF for Roman CGI. The add_distortion keyword must be set to False for this case.'
+ info_message = (
+ 'Geometric distortions are not implemented in WebbPSF for Roman CGI. '
+ 'The add_distortion keyword must be set to False for this case.'
)
+ _log.info(info_message)
# Run poppy calc_psf
psf = webbpsf_core.SpaceTelescopeInstrument.calc_psf(
diff --git a/webbpsf/tests/test_errorhandling.py b/webbpsf/tests/test_errorhandling.py
index e45d06ce..badd592d 100644
--- a/webbpsf/tests/test_errorhandling.py
+++ b/webbpsf/tests/test_errorhandling.py
@@ -12,6 +12,7 @@
_log = logging.getLogger('test_webbpsf')
_log.addHandler(logging.NullHandler())
+
def _exception_message_starts_with(excinfo, message_body):
return excinfo.value.args[0].startswith(message_body)
diff --git a/webbpsf/tests/test_fgs.py b/webbpsf/tests/test_fgs.py
index 03d370c0..c5ba7943 100644
--- a/webbpsf/tests/test_fgs.py
+++ b/webbpsf/tests/test_fgs.py
@@ -1,4 +1,3 @@
-
import logging
from .test_webbpsf import do_test_set_position_from_siaf, do_test_source_offset, generic_output_test
@@ -6,8 +5,8 @@
_log = logging.getLogger('test_webbpsf')
_log.addHandler(logging.NullHandler())
-# ------------------ FGS Tests ----------------------------
+# ------------------ FGS Tests ----------------------------
def test_fgs():
return generic_output_test('FGS')
@@ -21,6 +20,5 @@ def test_fgs_source_offset_45():
def test_fgs_set_siaf():
- return do_test_set_position_from_siaf(
- 'FGS', ['FGS1_FP1MIMF', 'FGS2_SUB128CNTR', 'FGS1_SUB128LL', 'FGS2_SUB32DIAG']
- )
+ return do_test_set_position_from_siaf('FGS',
+ ['FGS1_FP1MIMF', 'FGS2_SUB128CNTR', 'FGS1_SUB128LL', 'FGS2_SUB32DIAG'])
diff --git a/webbpsf/tests/test_miri.py b/webbpsf/tests/test_miri.py
index ff3c9568..12d70fc7 100644
--- a/webbpsf/tests/test_miri.py
+++ b/webbpsf/tests/test_miri.py
@@ -14,6 +14,7 @@
# ------------------ MIRI Tests ----------------------------
+
def test_miri():
return generic_output_test('MIRI')
@@ -27,15 +28,15 @@ def test_miri_source_offset_45():
def test_miri_set_siaf():
- return do_test_set_position_from_siaf(
- 'MIRI',
- [
- 'MIRIM_SUB128',
- 'MIRIM_FP1MIMF',
- 'MIRIM_BRIGHTSKY',
- 'MIRIM_TASLITLESSPRISM',
- ],
- )
+ return do_test_set_position_from_siaf(
+ 'MIRI',
+ [
+ 'MIRIM_SUB128',
+ 'MIRIM_FP1MIMF',
+ 'MIRIM_BRIGHTSKY',
+ 'MIRIM_TASLITLESSPRISM',
+ ],
+ )
def do_test_miri_fqpm(
@@ -110,7 +111,7 @@ def test_miri_slit_apertures():
assert np.isclose(miri._tel_coords()[1].to_value(u.arcsec), ap.V3Ref)
# Test we can switch back from SLIT to regular type apertures without any error
- miri.set_position_from_aperture_name("MIRIM_SLIT")
+ miri.set_position_from_aperture_name('MIRIM_SLIT')
miri.set_position_from_aperture_name('MIRIM_FULL')
@@ -135,6 +136,7 @@ def test_miri_nonsquare_detector():
miri.detector_position = (1023, 1031) # recall this is X, Y order
assert miri.detector_position == (1023, 1031)
+
def test_mode_switch():
"""Test switching between imaging and IFU modes, and switching IFU bands
Also checks this works to switch aperturenane, and conversely setting aperturename switches mode if needed.
@@ -147,23 +149,22 @@ def test_mode_switch():
# Explicitly switch mode to IFU
miri.mode = 'IFU'
assert 'IFU' in miri.aperturename
- assert miri.detector =='MIRIFUSHORT'
+ assert miri.detector == 'MIRIFUSHORT'
assert miri.aperturename.startswith('MIRIFU_CH')
assert miri._rotation != imager_rotation
assert miri.pixelscale > imager_pixelscale
# Explicitly switch back to imaging
miri.mode = 'imaging'
assert 'IFU' not in miri.aperturename
- assert miri.detector =='MIRIM'
+ assert miri.detector == 'MIRIM'
assert miri.aperturename.startswith('MIRIM_')
assert miri._rotation == imager_rotation
assert miri.pixelscale == imager_pixelscale
-
- # Implicitly switch to IFU
+ # Implicitly switch to IFU
miri.set_position_from_aperture_name('MIRIFU_CHANNEL3B')
assert 'IFU' in miri.aperturename
- assert miri.detector =='MIRIFULONG'
+ assert miri.detector == 'MIRIFULONG'
assert miri.aperturename == 'MIRIFU_CHANNEL3B'
assert miri._rotation != imager_rotation
assert miri.pixelscale > imager_pixelscale
@@ -172,7 +173,7 @@ def test_mode_switch():
# LRS is an odd case, SLIT aper type but operates like in imaging mode
miri.set_position_from_aperture_name('MIRIM_SLIT')
assert 'IFU' not in miri.aperturename
- assert miri.detector =='MIRIM'
+ assert miri.detector == 'MIRIM'
assert miri.aperturename.startswith('MIRIM_')
assert miri._rotation == imager_rotation
assert miri.pixelscale == imager_pixelscale
@@ -180,7 +181,7 @@ def test_mode_switch():
# And back to IFU again:
miri.mode = 'IFU'
assert 'IFU' in miri.aperturename
- assert miri.detector =='MIRIFUSHORT'
+ assert miri.detector == 'MIRIFUSHORT'
assert miri.aperturename.startswith('MIRIFU_CH')
assert miri._rotation != imager_rotation
assert miri.pixelscale > imager_pixelscale
@@ -189,30 +190,29 @@ def test_mode_switch():
miri.band = '4C'
assert miri.detector == 'MIRIFULONG'
assert miri.aperturename == 'MIRIFU_CHANNEL4C'
- assert miri.pixelscale > 3*imager_pixelscale
+ assert miri.pixelscale > 3 * imager_pixelscale
miri.band = '2A'
assert miri.detector == 'MIRIFUSHORT'
assert miri.aperturename == 'MIRIFU_CHANNEL2A'
- assert imager_pixelscale < miri.pixelscale < 2*imager_pixelscale
-
+ assert imager_pixelscale < miri.pixelscale < 2 * imager_pixelscale
# Test also we can switch to LRS, and then back to a regular imaging aperture
# this tests another edge case for aperture and pixel scale switches
miri.set_position_from_aperture_name('MIRIM_SLIT')
assert 'IFU' not in miri.aperturename
- assert miri.detector =='MIRIM'
+ assert miri.detector == 'MIRIM'
assert miri.aperturename == 'MIRIM_SLIT'
assert miri.pixelscale == imager_pixelscale
miri.set_position_from_aperture_name('MIRIM_FULL')
assert 'IFU' not in miri.aperturename
- assert miri.detector =='MIRIM'
+ assert miri.detector == 'MIRIM'
assert miri.aperturename == 'MIRIM_FULL'
assert miri.pixelscale == imager_pixelscale
def test_IFU_wavelengths():
- """ Test computing the wqvelength sampling for a sim IFU cube """
+ """Test computing the wqvelength sampling for a sim IFU cube"""
miri = webbpsf_core.MIRI()
# check mode swith to IFU
miri.mode = 'IFU'
diff --git a/webbpsf/tests/test_nircam.py b/webbpsf/tests/test_nircam.py
index 51724a15..323e1a9b 100644
--- a/webbpsf/tests/test_nircam.py
+++ b/webbpsf/tests/test_nircam.py
@@ -19,27 +19,34 @@
# ------------------ NIRCam Tests ----------------------------
+
def _close_enough(a, b):
# 1.5% variance accomodates the differences between the various NRC detectors in each channel
return np.isclose(a, b, rtol=0.015)
+
def test_nircam():
return generic_output_test('NIRCam')
+
def test_nircam_source_offset_00():
return do_test_source_offset('NIRCam', theta=0.0, monochromatic=2e-6)
+
def test_nircam_source_offset_45():
return do_test_source_offset('NIRCam', theta=45.0, monochromatic=2e-6)
+
def test_nircam_set_siaf():
- return do_test_set_position_from_siaf(
- 'NIRCam', ['NRCA5_SUB160', 'NRCA3_DHSPIL_SUB96', 'NRCA5_MASKLWB_F300M', 'NRCA2_TAMASK210R']
- )
+ return do_test_set_position_from_siaf(
+ 'NIRCam', ['NRCA5_SUB160', 'NRCA3_DHSPIL_SUB96', 'NRCA5_MASKLWB_F300M', 'NRCA2_TAMASK210R']
+ )
+
def test_nircam_blc_circ_45():
return do_test_nircam_blc(kind='circular', angle=45)
+
def test_nircam_blc_circ_0():
return do_test_nircam_blc(kind='circular', angle=0)
@@ -51,6 +58,7 @@ def test_nircam_blc_wedge_0(**kwargs):
def test_nircam_blc_wedge_45(**kwargs):
return do_test_nircam_blc(kind='linear', angle=-45, **kwargs)
+
# The test setup for this one is not quite right yet
# See https://github.com/mperrin/webbpsf/issues/30
# and https://github.com/mperrin/poppy/issues/29
@@ -187,9 +195,13 @@ def do_test_nircam_blc(clobber=False, kind='circular', angle=0, save=False, disp
# FIXME tolerance temporarily increased to 1% in final flux, to allow for using
# regular propagation rather than semi-analytic. See poppy issue #169
+ assert_string = (
+ f'Total flux {totflux} is out of tolerance relative to expectations '
+ f'{exp_flux}, for offset={offset}, angle={angle}'
+ )
assert (
abs(totflux - exp_flux) < 1e-4
- ), f'Total flux {totflux} is out of tolerance relative to expectations {exp_flux}, for offset={offset}, angle={angle}'
+ ), assert_string
# assert( abs(totflux - exp_flux) < 1e-2 )
_log.info('File {0} has the expected total flux based on prior reference calculation: {1}'.format(fnout, totflux))
@@ -318,7 +330,8 @@ def test_defocus(fov_arcsec=1, display=False):
via either a weak lens, or via the options dict,
and we get consistent results either way.
- Note this is now an *inexact* comparison, because the weak lenses now include non-ideal effects, in particular field dependent astigmatism
+ Note this is now an *inexact* comparison, because the weak lenses now include non-ideal effects,
+ in particular field dependent astigmatism
Test for #59 among other things
"""
@@ -636,10 +649,11 @@ def test_ways_to_specify_detectors():
nrc = webbpsf_core.NIRCam()
nrc.detector = 'NRCALONG'
- assert nrc.detector == 'NRCA5', "NRCALONG should be synonymous to NRCA5"
+ assert nrc.detector == 'NRCA5', 'NRCALONG should be synonymous to NRCA5'
nrc.detector = 'nrcblong'
- assert nrc.detector == 'NRCB5', "nrcblong should be synonymous to nrcb5"
+
+ assert nrc.detector == 'NRCB5', 'nrcblong should be synonymous to nrcb5'
def test_dhs():
diff --git a/webbpsf/tests/test_niriss.py b/webbpsf/tests/test_niriss.py
index 01dc487b..19979683 100644
--- a/webbpsf/tests/test_niriss.py
+++ b/webbpsf/tests/test_niriss.py
@@ -1,4 +1,3 @@
-
import numpy as np
from .. import webbpsf_core
@@ -6,20 +5,24 @@
# ------------------ NIRISS Tests ----------------------------
+
def test_niriss():
return generic_output_test('NIRISS')
+
def test_niriss_source_offset_00():
return do_test_source_offset('NIRISS', theta=0.0, monochromatic=3.0e-6)
+
def test_niriss_source_offset_45():
return do_test_source_offset('NIRISS', theta=45.0, monochromatic=3.0e-6)
+
def test_niriss_set_siaf():
- return do_test_set_position_from_siaf(
- 'NIRISS',
- ['NIS_FP1MIMF', 'NIS_SUB64', 'NIS_SOSSFULL', 'NIS_SOSSTA', 'NIS_AMI1']
- )
+ return do_test_set_position_from_siaf(
+ 'NIRISS',
+ ['NIS_FP1MIMF', 'NIS_SUB64', 'NIS_SOSSFULL', 'NIS_SOSSTA', 'NIS_AMI1']
+ )
def test_niriss_auto_pupil():
diff --git a/webbpsf/tests/test_nirspec.py b/webbpsf/tests/test_nirspec.py
index c3cf866e..53d18ad4 100644
--- a/webbpsf/tests/test_nirspec.py
+++ b/webbpsf/tests/test_nirspec.py
@@ -13,17 +13,22 @@
# ------------------ NIRSpec Tests ----------------------------
+
def test_nirspec():
return generic_output_test('NIRSpec')
+
+
# Use a larger than typical tolerance when testing NIRSpec offsets. The
# pixels are so undersampled (0.1 arcsec!) that it's unreasonable to try for
# better than 1/10th of a pixel precision using default settings.
def test_nirspec_source_offset_00():
return do_test_source_offset('NIRSpec', theta=0.0, tolerance=0.1, monochromatic=3e-6)
+
def test_nirspec_source_offset_45():
return do_test_source_offset('NIRSpec', theta=45.0, tolerance=0.1, monochromatic=3e-6)
+
def test_nirspec_set_siaf():
return do_test_set_position_from_siaf('NIRSpec')
@@ -51,11 +56,10 @@ def test_calc_datacube_fast():
waves = np.linspace(3e-6, 5e-6, 3)
nrs.calc_datacube_fast(waves, fov_pixels=30, oversample=1, compare_methods=True) # TODO assert success
-
def test_mode_switch():
- """ Test switch between IFU and imaging modes """
+ """Test switch between IFU and imaging modes"""
nrs = webbpsf_core.NIRSpec()
# check mode swith to IFU
nrs.mode = 'IFU'
@@ -72,8 +76,9 @@ def test_mode_switch():
nrs.mode = 'imaging'
assert 'IFU' not in nrs.aperturename
+
def test_IFU_wavelengths():
- """ Test computing the wqvelength sampling for a sim IFU cube """
+ """Test computing the wqvelength sampling for a sim IFU cube"""
nrs = webbpsf_core.NIRSpec()
# check mode swith to IFU
nrs.mode = 'IFU'
diff --git a/webbpsf/tests/test_opds.py b/webbpsf/tests/test_opds.py
index a56dcb69..368263ca 100644
--- a/webbpsf/tests/test_opds.py
+++ b/webbpsf/tests/test_opds.py
@@ -391,9 +391,13 @@ def test_apply_field_dependence_model():
# The value used in the following test is derived from this model itself, so it's a bit circular;
# but at least this test should suffice to detect any unintended significant change in the
# outputs of this model
+ assert_message = (
+ "Field-dependent OTE WFE at selected field point (NIRISS center) "
+ "didn't match expected value (test case: explicit call, assume_si_focus=False)"
+ )
assert np.isclose(
rms3, 36.0e-9, atol=1e-9
- ), "Field-dependent OTE WFE at selected field point (NIRISS center) didn't match expected value (test case: explicit call, assume_si_focus=False)"
+ ), assert_message
# Now test as usd in a webbpsf calculation, implicitly, and with the defocus backout ON
# The WFE here is slightly less, due to the focus optimization
@@ -406,9 +410,13 @@ def test_apply_field_dependence_model():
ote.update_opd()
opd_nis_cen_v2 = ote.opd
rms4 = rms(opd_nis_cen_v2, mask)
+ assert_message = (
+ "Field-dependent OTE WFE at selected field point (NIRISS center) "
+ "didn't match expected value(test case: implicit call, assume_si_focus=True)"
+ )
assert np.isclose(
rms4, 28.0e-9, atol=1e-9
- ), "Field-dependent OTE WFE at selected field point (NIRISS center) didn't match expected value(test case: implicit call, assume_si_focus=True."
+ ), assert_message
def test_get_zernike_coeffs_from_smif():
diff --git a/webbpsf/tests/test_utils.py b/webbpsf/tests/test_utils.py
index 5edea26a..bcc5e452 100644
--- a/webbpsf/tests/test_utils.py
+++ b/webbpsf/tests/test_utils.py
@@ -9,6 +9,7 @@
_log = logging.getLogger('test_webbpsf')
_log.addHandler(logging.NullHandler())
+
def test_logging_restart():
"""Test turning off and on the logging, and then put it back the way it was."""
level = conf.logging_level
diff --git a/webbpsf/tests/test_webbpsf.py b/webbpsf/tests/test_webbpsf.py
index 995b95b4..bcd68c65 100644
--- a/webbpsf/tests/test_webbpsf.py
+++ b/webbpsf/tests/test_webbpsf.py
@@ -10,6 +10,7 @@
_log = logging.getLogger('test_webbpsf')
_log.addHandler(logging.NullHandler())
+
# The following functions are used in each of the test_ files to
# test the individual SIs
def generic_output_test(iname):
@@ -214,7 +215,7 @@ def test_calc_psf_format_output():
def test_instrument():
- webbpsf_core.instrument('NIRCam') # TODO - assert success
+ webbpsf_core.instrument('NIRCam') # TODO - assert success
try:
import pytest
diff --git a/webbpsf/trending.py b/webbpsf/trending.py
index 7345d557..febcd6dc 100644
--- a/webbpsf/trending.py
+++ b/webbpsf/trending.py
@@ -89,16 +89,16 @@ def wavefront_time_series_plot(
'2022-03-22T22:55:00': ('Cooler State 4', 'red'),
'2022-04-10T17:24:00': ('Cooler State 6', 'red'),
'2022-04-07T20:13:00': ('Cooler State 5', 'red'),
- #'2022-04-15T06:33:00': ("NIRISS FPA Heater on (NIS-24)", 'orange'),
+ # '2022-04-15T06:33:00': ("NIRISS FPA Heater on (NIS-24)", 'orange'),
# '2022-04-10T04:30': ('NIRSpec FPA CCH to level 30','orange'),
- #'2022-04-10T01:00': ('NIRSpc bench & FPA heaters adjust', 'orange'), # to level 10
- #'2022-04-11T16:00:00': ('FGS trim heater 0 to 4', 'orange'),
+ # '2022-04-10T01:00': ('NIRSpc bench & FPA heaters adjust', 'orange'), # to level 10
+ # '2022-04-11T16:00:00': ('FGS trim heater 0 to 4', 'orange'),
'2022-04-23T06:30:00': ('OTE alignment complete', 'green'),
'2022-05-12T18:30:00': ('Thermal slew cold attitude start', 'blue'),
'2022-05-20T06:30:00': ('Thermal slew cold attitude end', 'blue'),
'2022-05-23T00:00:00': ('Larger micrometeorite strike on C3', 'red'),
'2022-06-19T18:00:00': ('Coarse move of C3 for astigmatism correction', 'red'),
- #'2022-06-27T00:00:00': ('NIRSpec safing, not in thermal control', 'orange'),
+ # '2022-06-27T00:00:00': ('NIRSpec safing, not in thermal control', 'orange'),
'2022-07-12T00:00:00': ('Large outlier tilt event on B5+C5', 'orange'),
'2024-02-25T20:00:00': ('Large outlier wing tilt event on -V2 wing', 'orange'),
}
@@ -379,7 +379,7 @@ def wfe_histogram_plot(
for i, idate in enumerate(where_post):
if idate:
- xtmp = dates[i - 1 : i + 1]
+ xtmp = dates[i - 1: i + 1]
ytmp = [rms_nm[i - 1] - yoffsets[1], rms_nm[i] + yoffsets[1]]
axes[0].plot(xtmp.plot_date, ytmp, color='limegreen', lw=2, ls='-')
@@ -458,7 +458,7 @@ def wfe_histogram_plot(
axes[0].legend()
-##### Wavefront Drifts Plot #####
+# Wavefront Drifts Plot #####
def show_opd_image(
@@ -695,7 +695,7 @@ def single_measurement_trending_plot(
else:
show_correction = False
- ############## Plotting
+ # Plotting ###########
# Plot setup
# fig, axes = plt.subplots(figsize=(8.5,11), nrows=3, ncols=4)
fig, axes = plt.subplots(
@@ -710,7 +710,7 @@ def single_measurement_trending_plot(
title += '\nNIRCam FP1 Target Phase Map Subtracted'
plt.suptitle(title, fontweight='bold', fontsize=18)
- ####### Row 1: Latest measurement, and correction if present
+ # Row 1: Latest measurement, and correction if present #####
fontsize = 11
# Panel 1: latest OPD
iax = axes[0, 0]
@@ -756,7 +756,7 @@ def single_measurement_trending_plot(
fig.text(
0.55, 0.77, 'Sensing-only visit. No mirror moves.', alpha=0.3, horizontalalignment='center', fontsize=fontsize
)
- ####### Row 2
+ # Row 2 #####
# Compare to immediate prior OPD
# Panel 2-1: prior OPD
@@ -779,7 +779,7 @@ def single_measurement_trending_plot(
show_opd_image(delta_opd - fit, ax=iax, vmax=vmax, fontsize=fontsize)
iax.set_title('High order WFE\nin difference', fontsize=fontsize * 1.1)
- ####### Row 3
+ # Row 3 #####
# Panel 3-1: ref OPD
iax = axes[2, 0]
@@ -939,7 +939,7 @@ def vprint(*text):
sum(which_opds_mask)
- ### Iterate over all relevant OPDs to retrieve the OPD, and calc the WFE RMS
+ # Iterate over all relevant OPDs to retrieve the OPD, and calc the WFE RMS
# Setup arrays to store results from the iteration:
n = np.sum(which_opds_mask)
rms_obs = np.zeros(n)
@@ -1060,7 +1060,7 @@ def vprint(*text):
plt.savefig('wf_drifts.pdf')
-##### Monthly Trending Plots, including OPDs, RMS WFE and PSF EE
+# Monthly Trending Plots, including OPDs, RMS WFE and PSF EE
def get_month_start_end(year, month):
@@ -1089,7 +1089,7 @@ def filter_opdtable_for_daterange(start_date, end_date, opdtable):
# we'll use this to compute the drift for the first WFS in the time period
is_pre = [astropy.time.Time(row['date']) < start_date for row in opdtable]
opdtable['is_pre'] = is_pre
- opdtable = opdtable[np.sum(is_pre) - 1 :]
+ opdtable = opdtable[np.sum(is_pre) - 1:]
return opdtable
@@ -1332,7 +1332,7 @@ def basic_show_image(image, ax, vmax=0.3, nanmask=1):
fig.suptitle(f'WF Trending for {year}-{month:02d}', fontsize=fs * 1.5, fontweight='bold')
- #### Plot 1: Wavefront Error
+ # Plot 1: Wavefront Error
axes[0].plot_date(dates_array.plot_date, rms_obs * 1e9, color='C1', ls='-', label='Observatory WFE at NIRCam NRCA3')
axes[0].plot_date(dates_array.plot_date, rms_ote * 1e9, color='C0', ls='-', label='Telescope WFE')
@@ -1356,7 +1356,7 @@ def basic_show_image(image, ax, vmax=0.3, nanmask=1):
axes[0].set_ylabel('Wavefront Error\n[nm rms]', fontsize=fs, fontweight='bold')
axes[0].set_xticklabels([])
- #### Plot 2: Encircled Energies
+ # Plot 2: Encircled Energies
ee_ax_ylim = 0.04
ee_measurements = {}
@@ -1690,8 +1690,12 @@ def plot_wfs_obs_delta(fn1, fn2, vmax_fraction=1.0, download_opds=True):
axes[0, 1].set_title(f'Measured: \n{os.path.basename(fn2)}', fontsize=18)
axes[0, 2].set_title('Difference\n ', fontsize=18)
+ figure_title = (
+ f"{hdul1[0].header['CORR_ID']}, {hdul1[0].header['TSTAMP'][:-3]} vs. "
+ f"{hdul2[0].header['CORR_ID']}, {hdul2[0].header['TSTAMP'][:-3]}"
+ )
fig.suptitle(
- f"{hdul1[0].header['CORR_ID']}, {hdul1[0].header['TSTAMP'][:-3]} vs. {hdul2[0].header['CORR_ID']}, {hdul2[0].header['TSTAMP'][:-3]}",
+ figure_title,
fontsize=20,
fontweight='bold',
)
@@ -2017,7 +2021,7 @@ def delta_wfe_around_time(datetime, plot=True, ax=None, vmax=0.05, return_filena
return delta_opd
-#### Functions for image comparisons
+# Functions for image comparisons
def show_nrc_ta_img(visitid, ax=None, return_handles=False):
"""Retrieve and display a NIRCam target acq image"""
@@ -2096,7 +2100,7 @@ def nrc_ta_image_comparison(visitid, verbose=False, show_centroids=False):
# Plot
if show_centroids:
- ### OSS CENTROIDS ###
+ # OSS CENTROIDS ###
# First, see if we can retrieve the on-board TA centroid measurment from the OSS engineering DB in MAST
try:
import misc_jwst # Optional dependency, including engineering database access tools
@@ -2122,7 +2126,8 @@ def nrc_ta_image_comparison(visitid, verbose=False, show_centroids=False):
if verbose:
print(f'OSS centroid on board: {oss_cen} (full det coord frame, 1-based)')
print(
- f'OSS centroid converted: {oss_cen_sci_pythonic} (sci frame in {nrc._detector_geom_info.aperture.AperName}, 0-based)'
+ f'OSS centroid converted: {oss_cen_sci_pythonic}',
+ f'(sci frame in {nrc._detector_geom_info.aperture.AperName}, 0-based)'
)
full_ap = nrc.siaf[nrc._detector_geom_info.aperture.AperName[0:5] + '_FULL']
oss_cen_full_sci = np.asarray(full_ap.det_to_sci(*oss_cen)) - 1
@@ -2133,7 +2138,7 @@ def nrc_ta_image_comparison(visitid, verbose=False, show_centroids=False):
print('Could not parse TA coordinates from log. TA may have failed?')
oss_cen_sci_pythonic = None
- ### WCS COORDINATES ###
+ # WCS COORDINATES ###
import jwst.datamodels
model = jwst.datamodels.open(hdul)
@@ -2166,7 +2171,7 @@ def nrc_ta_image_comparison(visitid, verbose=False, show_centroids=False):
except ImportError:
oss_centroid_text = ''
- ### WEBBPSF CENTROIDS ###
+ # WEBBPSF CENTROIDS ###
cen = webbpsf.fwcentroid.fwcentroid(im_obs_clean)
axes[0].scatter(cen[1], cen[0], color='red', marker='+', s=50)
axes[0].text(cen[1], cen[0], ' webbpsf', color='red', verticalalignment='center')
diff --git a/webbpsf/utils.py b/webbpsf/utils.py
index 590d013b..2b593d69 100644
--- a/webbpsf/utils.py
+++ b/webbpsf/utils.py
@@ -21,7 +21,7 @@
_Strehl_perfect_cache = {} # dict for caching perfect images used in Strehl calcs.
-### Helper routines for logging: ###
+# Helper routines for logging: ###
class FilterLevelRange(object):
@@ -160,7 +160,7 @@ def setup_logging(level='INFO', filename=None):
restart_logging(verbose=True)
-### Helper routines for data handling and system setup: ###
+# Helper routines for data handling and system setup: ###
MISSING_WEBBPSF_DATA_MESSAGE = """
*********** ERROR ****** ERROR ****** ERROR ****** ERROR ***********
@@ -443,7 +443,7 @@ def system_diagnostic():
return result
-### Helper routines for image manipulation: ###
+# Helper routines for image manipulation: ###
def rms(opd, mask):
@@ -548,8 +548,8 @@ def measure_strehl(HDUlist_or_filename=None, ext=0, slice=0, center=None, displa
# average across a group of 4
bot = [int(np.floor(f)) for f in center]
top = [int(np.ceil(f) + 1) for f in center]
- meas_peak = image[bot[1] : top[1], bot[0] : top[0]].mean()
- ref_peak = comparison_image[bot[1] : top[1], bot[0] : top[0]].mean()
+ meas_peak = image[bot[1]: top[1], bot[0]: top[0]].mean()
+ ref_peak = comparison_image[bot[1]: top[1], bot[0]: top[0]].mean()
strehl = meas_peak / ref_peak
if display:
@@ -649,7 +649,7 @@ def border_extrapolate_pad(image, mask):
return image_extrapolated
-### Helper routines for display customization: ###
+# Helper routines for display customization: ###
# use via poppy's display_annotate feature by assigning these to
# the display_annotate attribute of an OpticalElement class
@@ -997,7 +997,7 @@ def determine_inst_name_from_v2v3(v2v3):
return instrument
-def label_wavelength (nwavelengths, wavelength_slices):
+def label_wavelength(nwavelengths, wavelength_slices):
# Allow up to 10,000 wavelength slices. The number matters because FITS
# header keys can only have up to 8 characters. Backward-compatible.
if nwavelengths < 100:
diff --git a/webbpsf/webbpsf_core.py b/webbpsf/webbpsf_core.py
index 91b1d8da..8817aa1e 100644
--- a/webbpsf/webbpsf_core.py
+++ b/webbpsf/webbpsf_core.py
@@ -77,6 +77,7 @@ class SpaceTelescopeInstrument(poppy.instrument.Instrument):
configuration can be done by editing the :ref:`SpaceTelescopeInstrument.options` dictionary, either by
passing options to ``__init__`` or by directly editing the dict afterwards.
"""
+
telescope = 'Generic Space Telescope'
options = {} # options dictionary
""" A dictionary capable of storing other arbitrary options, for extensibility. The following are all optional, and
@@ -170,7 +171,8 @@ def __init__(self, name='', pixelscale=0.064):
self.name = name
self._WebbPSF_basepath, self._data_version = utils.get_webbpsf_data_path(
- data_version_min=DATA_VERSION_MIN, return_version=True)
+ data_version_min=DATA_VERSION_MIN, return_version=True
+ )
self._datapath = os.path.join(self._WebbPSF_basepath, self.name)
self._image_mask = None
@@ -300,7 +302,8 @@ def detector_position(self, position):
if x < 0 or y < 0:
raise ValueError('Detector pixel coordinates must be nonnegative integers')
if isinstance(self._detector_npixels, tuple):
- det_npix_y, det_npix_x = self._detector_npixels # A tuple has been provided for a non-square detector with different Y and X dimensions
+ # A tuple has been provided for a non-square detector with different Y and X dimensions
+ det_npix_y, det_npix_x = self._detector_npixels
else:
det_npix_y = det_npix_x = self._detector_npixels # same dimensions in both X and Y
@@ -386,8 +389,7 @@ def get_optical_system(self, fft_oversample=2, detector_oversample=None, fov_arc
_log.info('Creating optical system model:')
- self._extra_keywords = OrderedDict() # Place to save info we later want to put
- # into the FITS header for each PSF.
+ self._extra_keywords = OrderedDict() # Place to save info we later want to put into the FITS header for each PSF.
if options is None:
options = self.options
@@ -396,8 +398,7 @@ def get_optical_system(self, fft_oversample=2, detector_oversample=None, fov_arc
_log.debug('Oversample: %d %d ' % (fft_oversample, detector_oversample))
optsys = poppy.OpticalSystem(
- name='{telescope}+{instrument}'.format(telescope=self.telescope, instrument=self.name),
- oversample=fft_oversample
+ name='{telescope}+{instrument}'.format(telescope=self.telescope, instrument=self.name), oversample=fft_oversample
)
# For convenience offsets can be given in cartesian or radial coords
if 'source_offset_x' in options or 'source_offset_y' in options:
@@ -500,20 +501,20 @@ def get_optical_system(self, fft_oversample=2, detector_oversample=None, fov_arc
else:
pass
- optsys.add_detector(self.pixelscale,
- fov_pixels=fov_pixels,
- oversample=detector_oversample,
- name=self.name + " detector")
+ optsys.add_detector(
+ self.pixelscale,
+ fov_pixels=fov_pixels,
+ oversample=detector_oversample,
+ name=self.name + ' detector'
+ )
# --- invoke semi-analytic coronagraphic propagation
if trySAM and not ('no_sam' in self.options and self.options['no_sam']):
# if this flag is set, try switching to SemiAnalyticCoronagraph mode.
_log.info('Trying to invoke switch to Semi-Analytic Coronagraphy algorithm')
try:
- SAM_optsys = poppy.SemiAnalyticCoronagraph(optsys,
- oversample=fft_oversample,
- occulter_box=SAM_box_size)
- _log.info("SAC OK")
+ SAM_optsys = poppy.SemiAnalyticCoronagraph(optsys, oversample=fft_oversample, occulter_box=SAM_box_size)
+ _log.info('SAC OK')
return SAM_optsys
except ValueError as err:
_log.warning(
@@ -726,10 +727,11 @@ def psf_grid(
if single_psf_centered is True:
if isinstance(self._detector_npixels, tuple):
- det_npix_y, det_npix_x = self._detector_npixels # A tuple has been provided for a non-square detector with different Y and X dimensions
+ # A tuple has been provided for a non-square detector with different Y and X dimensions
+ det_npix_y, det_npix_x = self._detector_npixels
else:
det_npix_y = det_npix_x = self._detector_npixels # same dimensions in both X and Y
- psf_location = ( int(det_npix_x - 1) // 2, int(det_npix_y - 1) // 2) # center pt
+ psf_location = (int(det_npix_x - 1) // 2, int(det_npix_y - 1) // 2) # center pt
else:
psf_location = self.detector_position[::-1] # (y,x)
@@ -753,7 +755,7 @@ def psf_grid(
return gridmodel
-####### JWInstrument classes #####
+# JWInstrument classes #####
@utils.combine_docstrings
@@ -976,14 +978,20 @@ def aperturename(self, value):
)
if not has_custom_pixelscale:
self.pixelscale = self._get_pixelscale_from_apername(detector_apername)
- _log.debug(
- f'Pixelscale updated to {self.pixelscale} based on average X+Y SciScale at SIAF aperture {detector_apername}'
+ debug_message = (
+ f'Pixelscale updated to {self.pixelscale} '
+ f'based on average X+Y SciScale at SIAF aperture {detector_apername}'
)
- elif ap.AperType == 'COMPOUND' and self.name=='MIRI':
+ _log.debug(debug_message)
+ elif ap.AperType == 'COMPOUND' and self.name == 'MIRI':
# For MIRI, many of the relevant IFU apertures are of COMPOUND type.
has_custom_pixelscale = False # custom scales not supported for MIRI IFU (yet?)
# Unlike NIRSpec, there simply do not exist full-detector SIAF apertures for the MIRI IFU detectors
- _log.info(f'Aperture {value} is of type COMPOUND for MIRI; There do not exist corresponding SIAF apertures, so we ignore setting detector geometry.')
+ info_message = (
+ f'Aperture {value} is of type COMPOUND for MIRI; '
+ 'There do not exist corresponding SIAF apertures, so we ignore setting detector geometry.'
+ )
+ _log.info(info_message)
# Now apply changes:
self._aperturename = value
@@ -991,24 +999,29 @@ def aperturename(self, value):
self._detector_geom_info = DetectorGeometry(self.siaf, self._aperturename)
if not has_custom_pixelscale:
self.pixelscale = self._get_pixelscale_from_apername(value)
- _log.debug( f"Pixelscale updated to {self.pixelscale} based on IFU cubepars for {value}")
+ _log.debug(f'Pixelscale updated to {self.pixelscale} based on IFU cubepars for {value}')
else:
if self.detector not in value:
- raise ValueError(
+ error_message = (
f'Aperture name {value} does not match currently selected detector {self.detector}. '
f'Change detector attribute first, then set desired aperture.'
)
+ raise ValueError(error_message)
# 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
- # and also check first that it's not a SLIT type aperture, for which the usual _get_pixelscale_from_apername won't work.
- # and also check neither current nor requested aperture are of type SLIT since that doesn't have a pixelscale to get.
+ # Note:
+ # check self._aperturename first to account for the edge case when
+ # this is called from __init__ before _aperturename is set
+ # and also check first that it's not a SLIT type aperture,
+ # for which the usual _get_pixelscale_from_apername won't work.
+ # and also check neither current nor requested aperture are of type
+ # SLIT since that doesn't have a pixelscale to get.
has_custom_pixelscale = (
self._aperturename
- and (self.siaf[self._aperturename].AperType != 'SLIT')
+ and (self.siaf[self._aperturename].AperType != 'SLIT')
and (self.pixelscale != self._get_pixelscale_from_apername(self._aperturename))
and ap.AperType != 'SLIT'
)
@@ -1025,7 +1038,8 @@ def aperturename(self, value):
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}'
+ f'Pixelscale updated to {self.pixelscale}',
+ f'based on average X+Y SciScale at SIAF aperture {self._aperturename}'
)
def _tel_coords(self):
@@ -1039,11 +1053,14 @@ def _tel_coords(self):
if self._detector_geom_info.aperture.AperType == 'SLIT':
# These apertures don't map directly to particular detector position in the usual way
# Return coords for center of the aperture reference location
- return np.asarray((self._detector_geom_info.aperture.V2Ref,
- self._detector_geom_info.aperture.V3Ref)) / 60 * units.arcmin
+ return (
+ np.asarray((self._detector_geom_info.aperture.V2Ref, self._detector_geom_info.aperture.V3Ref))
+ / 60
+ * units.arcmin
+ )
elif self._detector_geom_info.aperture.AperType == 'COMPOUND':
# handle MIRI MRS apertures, which don't have V2Ref,V3Ref defined, but this works:
- return np.asarray(self.siaf[self.aperturename].reference_point('tel') ) / 60 * units.arcmin
+ return np.asarray(self.siaf[self.aperturename].reference_point('tel')) / 60 * units.arcmin
else:
return self._detector_geom_info.pix2angle(self.detector_position[0], self.detector_position[1])
@@ -1084,7 +1101,7 @@ def set_position_from_aperture_name(self, aperture_name):
self.aperturename = aperture_name
- if (self.name == 'NIRSpec' or self.name=='MIRI') and ap.AperType == 'SLIT':
+ if (self.name == 'NIRSpec' or self.name == 'MIRI') and ap.AperType == 'SLIT':
# NIRSpec slit apertures need some separate handling, since they don't map directly to detector pixels
# In this case the detector position is not uniquely defined, but we ensure to get reasonable values by
# using one of the full-detector NIRspec apertures
@@ -1255,7 +1272,9 @@ def _calc_psf_format_output(self, result, options):
# therefore omit apply_distortion if a SLIT aperture is selected.
psf_siaf = result
psf_siaf_rot = detectors.apply_miri_scattering(psf_siaf) # apply scattering effect
- psf_distorted = detectors.apply_detector_charge_diffusion(psf_siaf_rot,options) # apply detector charge transfer model
+ psf_distorted = detectors.apply_detector_charge_diffusion(
+ psf_siaf_rot, options
+ ) # apply detector charge transfer model
else:
# there is not yet any distortion calibration for the IFU, and
# we don't want to apply charge diffusion directly here
@@ -1442,7 +1461,9 @@ def _linear_smear(smear_length, image):
elif local_options['jitter'].lower() == 'pcs=coarse_like_itm':
# JWST coarse point, assumptions in ITM
# Acton says:
- # it is actually 0.4 for a boresight error, 0.4 smear, and 0.2 jitter. Boresight error is a random term for image placement, smear is mostly a linear uniform blur, and jitter is gaussian.
+ # it is actually 0.4 for a boresight error, 0.4 smear, and 0.2 jitter.
+ # Boresight error is a random term for image placement,
+ # smear is mostly a linear uniform blur, and jitter is gaussian.
# First we do the fast jitter part
local_options['jitter_sigma'] = 0.2
@@ -1598,7 +1619,8 @@ def load_wss_opd(self, filename, output_path=None, backout_si_wfe=True, verbose=
filename : str
Name of OPD file to load
output_path : str
- Downloaded OPD are saved in this location. This option is convinient for STScI users using /grp/jwst/ote/webbpsf-data/.
+ Downloaded OPD are saved in this location.
+ This option is convinient for STScI users using /grp/jwst/ote/webbpsf-data/.
Default is $WEBBPSF_PATH/MAST_JWST_WSS_OPDs
backout_si_wfe : bool
Subtract model for science instrument WFE at the sensing field point? Generally this should be true
@@ -1833,7 +1855,6 @@ def calc_datacube_fast(self, wavelengths, compare_methods=False, outfile=None, *
"""
-
nwavelengths = len(wavelengths)
# Set up cube and initialize structure based on PSF at a representative wavelength
@@ -1842,9 +1863,13 @@ def calc_datacube_fast(self, wavelengths, compare_methods=False, outfile=None, *
MIN_REF_WAVE = 2e-6 # This must not be too short, to avoid phase wrapping for the C3 bump
if ref_wave < MIN_REF_WAVE:
ref_wave = MIN_REF_WAVE
- _log.info(f"Performing initial propagation at minimum wavelength {MIN_REF_WAVE*1e6:.2f} microns; minimum set to avoid phase wrap of segment C3 surface.")
+ log_message = (
+ f'Performing initial propagation at minimum wavelength {MIN_REF_WAVE*1e6:.2f} microns; '
+ 'minimum set to avoid phase wrap of segment C3 surface.'
+ )
+ _log.info(log_message)
else:
- _log.info(f"Performing initial propagation at average wavelength {ref_wave*1e6:.2f} microns.")
+ _log.info(f'Performing initial propagation at average wavelength {ref_wave*1e6:.2f} microns.')
psf, waves = self.calc_psf(*args, monochromatic=ref_wave, return_intermediates=True, **kwargs)
from copy import deepcopy
@@ -1865,7 +1890,7 @@ def calc_datacube_fast(self, wavelengths, compare_methods=False, outfile=None, *
cubefast[ext].data[0] = psf[ext].data
cubefast[ext].header[label_wavelength(nwavelengths, 0)] = wavelengths[0]
- ### Fast way. Assumes wavelength-independent phase and amplitude at the exit pupil!!
+ # Fast way. Assumes wavelength-independent phase and amplitude at the exit pupil!!
if compare_methods:
import time
@@ -1898,8 +1923,8 @@ def calc_datacube_fast(self, wavelengths, compare_methods=False, outfile=None, *
cubefast[0].header['NWAVES'] = nwavelengths
- ### OPTIONAL
- ### Also do the slower traditional way for comparison / debugging tests
+ # OPTIONAL
+ # Also do the slower traditional way for comparison / debugging tests
if compare_methods:
psf2, waves2 = quickosys.calc_psf(wavelengths[0], return_intermediates=True)
@@ -1933,21 +1958,20 @@ def calc_datacube_fast(self, wavelengths, compare_methods=False, outfile=None, *
return cube, cubefast, waves, waves2 # return extra stuff for compariosns
if outfile is not None:
- cubefast[0].header["FILENAME"] = (os.path.basename(outfile), "Name of this file")
+ cubefast[0].header['FILENAME'] = (os.path.basename(outfile), 'Name of this file')
cubefast.writeto(outfile, overwrite=True)
- _log.info("Saved result to " + outfile)
+ _log.info('Saved result to ' + outfile)
return cubefast
-class JWInstrument_with_IFU(JWInstrument, ABC):
- """ Subclass which adds some additional infrastructure for IFU sims"""
+class JWInstrument_with_IFU(JWInstrument, ABC):
+ """Subclass which adds some additional infrastructure for IFU sims"""
def __init__(self, *args, **kwargs):
- super().__init__( *args, **kwargs)
+ super().__init__(*args, **kwargs)
# dict of modes and default aperture names
- self._modes_list = {'imaging': None,
- 'IFU': None}
+ self._modes_list = {'imaging': None, 'IFU': None}
self._mode = 'imaging'
self._IFU_bands_cubepars = {} # placeholder, subclass should implement
@@ -1957,7 +1981,6 @@ def mode(self):
"""Currently selected instrument major mode, imaging or IFU"""
return self._mode
-
@mode.setter
def mode(self, value):
if value not in self._modes_list:
@@ -1972,10 +1995,9 @@ def band(self):
pass
def get_IFU_wavelengths(self, nlambda=None):
- """Return an array of wavelengths spanning the currently selected IFU sub-band
- """
+ """Return an array of wavelengths spanning the currently selected IFU sub-band"""
if self.mode != 'IFU':
- raise RuntimeError("This method only applies in IFU mode")
+ raise RuntimeError('This method only applies in IFU mode')
spaxelsize, wavestep, minwave, maxwave = self._IFU_bands_cubepars[self.band]
if nlambda:
# Return the specified number of wavelengths, across that band
@@ -1987,7 +2009,7 @@ def get_IFU_wavelengths(self, nlambda=None):
class MIRI(JWInstrument_with_IFU):
- """ A class modeling the optics of MIRI, the Mid-InfraRed Instrument.
+ """A class modeling the optics of MIRI, the Mid-InfraRed Instrument.
Relevant attributes include `filter`, `image_mask`, and `pupil_mask`.
@@ -2013,10 +2035,13 @@ def __init__(self):
# This is rotation counterclockwise; when summed with V3PA it will yield the Y axis PA on sky
# Modes and default SIAF apertures for each
- self._modes_list = {'imaging': 'MIRIM_FULL',
- 'IFU': 'MIRIFU_CHANNEL1A'}
+ self._modes_list = {
+ 'imaging': 'MIRIM_FULL',
+ 'IFU': 'MIRIFU_CHANNEL1A'
+ }
- # Coordinate system note: The pupil shifts get applied at the instrument pupil, which is an image of the OTE exit pupil
+ # Coordinate system note:
+ # The pupil shifts get applied at the instrument pupil, which is an image of the OTE exit pupil
# and is thus flipped in Y relative to the V frame entrance pupil. Therefore flip sign of pupil_shift_y
self.options['pupil_shift_x'] = -0.0068 # In flight measurement. See Wright, Sabatke, Telfer 2022, Proc SPIE
self.options['pupil_shift_y'] = -0.0110 # Sign intentionally flipped relative to that paper!! See note above.
@@ -2041,45 +2066,48 @@ def __init__(self):
}
# The above tuples give the pixel resolution (first the 'alpha' direction, perpendicular to the slice,
# then the 'beta' direction, along the slice).
- # The pixels are not square. See https://jwst-docs.stsci.edu/jwst-mid-infrared-instrument/miri-observing-modes/miri-medium-resolution-spectroscopy
+ # The pixels are not square. See:
+ # https://jwst-docs.stsci.edu/jwst-mid-infrared-instrument/miri-observing-modes/miri-medium-resolution-spectroscopy
# Mappings between alternate names used for MRS subbands
- self._MRS_dichroic_to_subband = {"SHORT": "A", "MEDIUM": "B", "LONG": "C"}
- self._MRS_subband_to_dichroic = {"A": "SHORT", "B": "MEDIUM", "C": "LONG"}
+ self._MRS_dichroic_to_subband = {'SHORT': 'A', 'MEDIUM': 'B', 'LONG': 'C'}
+ self._MRS_subband_to_dichroic = {'A': 'SHORT', 'B': 'MEDIUM', 'C': 'LONG'}
self._band = None
-# self._MRS_bands = {"1A": [4.887326748103221, 5.753418963216559], # Values provided by Polychronis Patapis
-# "1B": [5.644625711181792, 6.644794583147869], # To-do: obtain from CRDS pipeline refs
-# "1C": [6.513777066360325, 7.669147994055998],
-# "2A": [7.494966046398437, 8.782517027772244],
-# "2B": [8.651469658142522, 10.168811217793243],
-# "2C": [9.995281242621394, 11.73039280033565],
-# "3A": [11.529088518317131, 13.491500288051483],
-# "3B": [13.272122736770127, 15.550153182343314],
-# "3C": [15.389530615108631, 18.04357852656418],
-# "4A": [17.686540162850203, 20.973301482912323],
-# "4B": [20.671069749545193, 24.476094964546686],
-# "4C": [24.19608171436692, 28.64871057821349]}
+ # self._MRS_bands = {"1A": [4.887326748103221, 5.753418963216559], # Values provided by Polychronis Patapis
+ # "1B": [5.644625711181792, 6.644794583147869], # To-do: obtain from CRDS pipeline refs
+ # "1C": [6.513777066360325, 7.669147994055998],
+ # "2A": [7.494966046398437, 8.782517027772244],
+ # "2B": [8.651469658142522, 10.168811217793243],
+ # "2C": [9.995281242621394, 11.73039280033565],
+ # "3A": [11.529088518317131, 13.491500288051483],
+ # "3B": [13.272122736770127, 15.550153182343314],
+ # "3C": [15.389530615108631, 18.04357852656418],
+ # "4A": [17.686540162850203, 20.973301482912323],
+ # "4B": [20.671069749545193, 24.476094964546686],
+ # "4C": [24.19608171436692, 28.64871057821349]}
self._IFU_bands_cubepars = { # pipeline data cube parameters
- # Taken from ifucubepars_table in CRDS file 'jwst_miri_cubepar_0014.fits', current as of 2023 December
- # Each tuple gives pipeline spaxelsize, spectralstep, wave_min, wave_max
- '1A': (0.13, 0.0008, 4.90, 5.74),
- '1B': (0.13, 0.0008, 5.66, 6.63),
- '1C': (0.13, 0.0008, 6.53, 7.65),
- '2A': (0.17, 0.0013, 7.51, 8.77),
- '2B': (0.17, 0.0013, 8.67, 10.13),
- '2C': (0.17, 0.0013, 10.01, 11.70),
- '3A': (0.20, 0.0025, 11.55, 13.47),
- '3B': (0.20, 0.0025, 13.34, 15.57),
- '3C': (0.20, 0.0025, 15.41, 17.98),
- '4A': (0.35, 0.0060, 17.70, 20.95),
- '4B': (0.35, 0.0060, 20.69, 24.48),
- '4C': (0.35, 0.0060, 24.40, 28.70),
+ # Taken from ifucubepars_table in CRDS file 'jwst_miri_cubepar_0014.fits', current as of 2023 December
+ # Each tuple gives pipeline spaxelsize, spectralstep, wave_min, wave_max
+ '1A': (0.13, 0.0008, 4.90, 5.74),
+ '1B': (0.13, 0.0008, 5.66, 6.63),
+ '1C': (0.13, 0.0008, 6.53, 7.65),
+ '2A': (0.17, 0.0013, 7.51, 8.77),
+ '2B': (0.17, 0.0013, 8.67, 10.13),
+ '2C': (0.17, 0.0013, 10.01, 11.70),
+ '3A': (0.20, 0.0025, 11.55, 13.47),
+ '3B': (0.20, 0.0025, 13.34, 15.57),
+ '3C': (0.20, 0.0025, 15.41, 17.98),
+ '4A': (0.35, 0.0060, 17.70, 20.95),
+ '4B': (0.35, 0.0060, 20.69, 24.48),
+ '4C': (0.35, 0.0060, 24.40, 28.70),
}
- self._detectors = {'MIRIM': 'MIRIM_FULL', # Mapping from user-facing detector names to SIAF entries.
- 'MIRIFUSHORT': 'MIRIFU_CHANNEL1A', # only applicable in IFU mode
- 'MIRIFULONG': 'MIRIFU_CHANNEL3A'} # ditto
+ self._detectors = {
+ 'MIRIM': 'MIRIM_FULL', # Mapping from user-facing detector names to SIAF entries.
+ 'MIRIFUSHORT': 'MIRIFU_CHANNEL1A', # only applicable in IFU mode
+ 'MIRIFULONG': 'MIRIFU_CHANNEL3A',
+ } # ditto
self.detector = 'MIRIM'
self._detector_npixels = (1032, 1024) # MIRI detector is not square
self.detector_position = (512, 512)
@@ -2203,7 +2231,8 @@ def make_fqpm_wrapper(name, wavelength):
)
if self.options.get('lrs_use_mft', True):
# Force the LRS slit to be rasterized onto a fine spatial sampling with gray subpixels
- # let's do a 3 arcsec box, sampled to 0.02 arcsec, with gray subpixels; note poppy does not support non-square wavefront here
+ # let's do a 3 arcsec box, sampled to 0.02 arcsec, with gray subpixels;
+ # note poppy does not support non-square wavefront here
lrs_pixscale = 0.02 # implicitly u.arcsec/u.pixel
sampling = poppy.Wavefront(npix=int(5.5 / lrs_pixscale), pixelscale=lrs_pixscale)
lrs_slit = poppy.fixed_sampling_optic(lrs_slit, sampling, oversample=8)
@@ -2345,10 +2374,10 @@ def _get_pixelscale_from_apername(self, apername):
if 'MIRIFU' in apername:
if apername.startswith('MIRIFU_CHANNEL'):
band = apername[-2:]
- spaxelsize, _, _, _= self._IFU_bands_cubepars[band]
+ spaxelsize, _, _, _ = self._IFU_bands_cubepars[band]
return spaxelsize
else:
- raise RuntimeError(f"Not sure how to determine pixelscale for {apername}")
+ raise RuntimeError(f'Not sure how to determine pixelscale for {apername}')
else:
return super()._get_pixelscale_from_apername(apername)
@@ -2382,12 +2411,11 @@ def _get_aperture_rotation(self, apername):
avg_V3IdlYangle = np.rad2deg((np.arctan2(dy, -dx) + np.arctan2(dy2, -dx2)) / 2)
return avg_V3IdlYangle
else:
- raise ValueError(f"Unexpected/invalid apername for MIRI: {apername}")
-
+ raise ValueError(f'Unexpected/invalid apername for MIRI: {apername}')
@JWInstrument_with_IFU.aperturename.setter
def aperturename(self, value):
- """Set aperturename, also update the rotation for MIRIM vs. IFU channel """
+ """Set aperturename, also update the rotation for MIRIM vs. IFU channel"""
# apply the provided aperture name
# Note, the syntax for calling a parent class property setter is... painful:
super(MIRI, type(self)).aperturename.fset(self, value)
@@ -2403,7 +2431,6 @@ def aperturename(self, value):
self.band = value[-2:]
self._detector = 'MIRIFULONG' if self.band[0] in ['3', '4'] else 'MIRIFUSHORT'
-
@property
def band(self):
"""MRS IFU spectral band. E.g. '1A', '3B'. Only applicable in IFU mode."""
@@ -2421,27 +2448,27 @@ def band(self, value):
if value in self._IFU_bands_cubepars.keys():
self._band = value
- #self._slice_width = self._IFU_pixelscale[f"Ch{self._band[0]}"][0]
+ # self._slice_width = self._IFU_pixelscale[f"Ch{self._band[0]}"][0]
self.aperturename = 'MIRIFU_CHANNEL' + value
# setting aperturename will also auto update self._rotation
- #self._rotation = self.MRS_rotation[self._band]
+ # self._rotation = self.MRS_rotation[self._band]
# update filter, image_mask and detector
- #self._filter = "D"+ self.subband_to_dichroic[self._band[1]]
- #self._image_mask = "MIRI-IFU_" + self._band[0]
- #self._update_detector()
- #if not (self.MRSbands[self.band][0] <= self._wavelength <= self.MRSbands[self.band][1]):
+ # self._filter = "D"+ self.subband_to_dichroic[self._band[1]]
+ # self._image_mask = "MIRI-IFU_" + self._band[0]
+ # self._update_detector()
+ # if not (self.MRSbands[self.band][0] <= self._wavelength <= self.MRSbands[self.band][1]):
# self._wavelength = np.mean(self.MRSbands[self.band])
else:
- raise ValueError(f"Not a valid MRS band: {value}")
+ raise ValueError(f'Not a valid MRS band: {value}')
def _calc_psf_format_output(self, result, options):
- """ Format output HDUList. In particular, add some extra metadata if in IFU mode"""
+ """Format output HDUList. In particular, add some extra metadata if in IFU mode"""
super()._calc_psf_format_output(result, options)
- if self.mode =='IFU':
+ if self.mode == 'IFU':
n_exts = len(result)
for ext in np.arange(n_exts):
- result[ext].header['MODE'] = ("IFU", 'This is a MIRI MRS IFU mode simulation')
- result[ext].header['FILTER'] = ('MIRIFU_CHANNEL'+self.band, 'MIRI IFU sub-band simulated')
+ result[ext].header['MODE'] = ('IFU', 'This is a MIRI MRS IFU mode simulation')
+ result[ext].header['FILTER'] = ('MIRIFU_CHANNEL' + self.band, 'MIRI IFU sub-band simulated')
result[ext].header['BAND'] = (self.band, 'MIRI IFU sub-band simulated')
@@ -2598,7 +2625,8 @@ def _update_aperturename(self):
else:
apname = 'NRCA2_FULL_WEDGE_RND' if self.module == 'A' else 'NRCB1_MASK210R'
_log.debug(
- f'Inferred {apname} from coronagraph Lyot mask selected, and channel={self.channel}, module={self.module}'
+ f'Inferred {apname} from coronagraph Lyot mask selected,',
+ f'and channel={self.channel}, module={self.module}'
)
else:
apname = self._detectors[self._detector]
@@ -2641,9 +2669,11 @@ def aperturename(self, value):
if newval is not None:
# Set alternative aperture name as bandaid to continue
value = newval
- _log.warning(
- 'Possibly running an old version of pysiaf missing some NIRCam apertures. Continuing with old aperture names.'
+ warning_message = (
+ 'Possibly running an old version of pysiaf missing some NIRCam apertures. '
+ 'Continuing with old aperture names.'
)
+ _log.warning(warning_message)
else:
return
@@ -2652,7 +2682,8 @@ def aperturename(self, 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
+ # 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)
)
@@ -2675,9 +2706,11 @@ def aperturename(self, value):
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}'
+ debug_message = (
+ f'Pixelscale updated to {self.pixelscale} '
+ f'based on average X+Y SciScale at SIAF aperture {self._aperturename}'
)
+ _log.debug(debug_message)
@property
def module(self):
@@ -2702,7 +2735,7 @@ def detector(self, value):
"""Set detector, including reloading the relevant info from SIAF"""
if value.upper().endswith('LONG'):
# treat NRCALONG and NRCBLONG as synonyms to NRCA5 and NRCB5
- value = value[:-4]+ '5'
+ value = value[:-4] + '5'
if value.upper() not in self.detector_list:
raise ValueError('Invalid detector. Valid detector names are: {}'.format(', '.join(self.detector_list)))
# set the channel based on the requested detector
@@ -2916,7 +2949,7 @@ def _addAdditionalOptics(self, optsys, oversample=2):
(self.pupil_mask is not None)
and ('LENS' not in self.pupil_mask.upper())
and ('WL' not in self.pupil_mask.upper())
- and ('DHS' not in self.pupil_mask.upper())
+ and ('DHS' not in self.pupil_mask.upper())
):
# no occulter selected but coronagraphic mode anyway. E.g. off-axis PSF
# but don't add this image plane for weak lens or DHS calculations
@@ -3025,8 +3058,15 @@ def _addAdditionalOptics(self, optsys, oversample=2):
elif self.pupil_mask is None and self.image_mask is not None:
optsys.add_pupil(poppy.ScalarTransmission(name='No Lyot Mask Selected!'), index=3)
elif self.pupil_mask.startswith('DHS'):
- optsys.add_pupil(transmission=self._datapath + f"/optics/NIRCam_{self.pupil_mask}_npix1024.fits.gz", name=self.pupil_mask,
- flip_y=True, shift_x=shift_x, shift_y=shift_y, rotation=rotation, index=3)
+ optsys.add_pupil(
+ transmission=self._datapath + f'/optics/NIRCam_{self.pupil_mask}_npix1024.fits.gz',
+ name=self.pupil_mask,
+ flip_y=True,
+ shift_x=shift_x,
+ shift_y=shift_y,
+ rotation=rotation,
+ index=3,
+ )
optsys.planes[3].wavefront_display_hint = 'intensity'
else:
@@ -3091,11 +3131,13 @@ def __init__(self):
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.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
+ # This is rotation counterclockwise; when summed with V3PA it will yield the Y axis PA on sky
# Modes and default SIAF apertures for each
- self._modes_list = {'imaging': 'NRS1_FULL',
- 'IFU': 'NRS_FULL_IFU'}
+ self._modes_list = {
+ 'imaging': 'NRS1_FULL',
+ 'IFU': 'NRS_FULL_IFU'
+ }
self._IFU_pixelscale = 0.1043 # same.
self.monochromatic = 3.0
@@ -3122,7 +3164,7 @@ def __init__(self):
self.disperser_list = ['PRISM', 'G140M', 'G140H', 'G235M', 'G235H', 'G394M', 'G395H']
self._disperser = None
- self._IFU_bands_cubepars = {
+ self._IFU_bands_cubepars = {
'PRISM/CLEAR': (0.10, 0.0050, 0.60, 5.30),
'G140M/F070LP': (0.10, 0.0006, 0.70, 1.27),
'G140M/F100LP': (0.10, 0.0006, 0.97, 1.89),
@@ -3192,11 +3234,12 @@ def _addAdditionalOptics(self, optsys, oversample=2):
if self.include_si_wfe:
optsys.add_pupil(optic=self._si_wfe_class(self, where='spectrograph'))
- if self.mode == 'IFU' and self.options.get('ifualign_rotation',True):
- optsys.add_rotation(90, hide=True) # Rotate by 90 degrees clockwise to match the IFUalign output convention, with slices horizontal.
+ if self.mode == 'IFU' and self.options.get('ifualign_rotation', True):
+ optsys.add_rotation(
+ 90, hide=True
+ ) # Rotate by 90 degrees clockwise to match the IFUalign output convention, with slices horizontal.
optsys.planes[-1].wavefront_display_hint = 'intensity'
-
return (optsys, trySAM, SAM_box_size)
def _get_fits_header(self, hdulist, options):
@@ -3205,7 +3248,6 @@ def _get_fits_header(self, hdulist, options):
hdulist[0].header['GRATING'] = ('None', 'NIRSpec grating element name')
hdulist[0].header['APERTURE'] = (str(self.image_mask), 'NIRSpec slit aperture name')
-
@JWInstrument.aperturename.setter
def aperturename(self, value):
"""Set SIAF aperture name to new value, with validation.
@@ -3222,7 +3264,8 @@ def aperturename(self, value):
except KeyError:
raise ValueError(f'Aperture name {value} not a valid SIAF aperture name for {self.name}')
- # NIRSpec apertures can either be per detector (i.e. "NRS1_FULL") or for the focal plane but not per detector (i.e. "NRS_FULL_IFU")
+ # NIRSpec apertures can either be per detector (i.e. "NRS1_FULL")
+ # or for the focal plane but not per detector (i.e. "NRS_FULL_IFU")
if value[0:4] in ['NRS1', 'NRS2']:
# this is a regular per-detector aperture, so just call the regular code in the superclass
@@ -3230,15 +3273,18 @@ def aperturename(self, value):
else:
# apertures that start with NRS define V2,V3 position, but not pixel coordinates and pixelscale. So we
# still have to use a full-detector aperturename for that.
- detector_apername = self.detector + "_FULL"
+ detector_apername = self.detector + '_FULL'
# Only update if new value is different
if self._aperturename != value:
# First, check some info from current settings, which 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(detector_apername))
+ # 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(detector_apername)
+ )
# Now apply changes:
self._aperturename = value
@@ -3247,11 +3293,15 @@ def aperturename(self, value):
# Update DetectorGeometry class
self._detector_geom_info = DetectorGeometry(self.siaf, self._aperturename)
- _log.info(f"{self.name} SIAF aperture name updated to {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(detector_apername)
- _log.debug(f"Pixelscale updated to {self.pixelscale} based on average X+Y SciScale at SIAF aperture {self._aperturename}")
+ debug_message = (
+ f'Pixelscale updated to {self.pixelscale} '
+ f'based on average X+Y SciScale at SIAF aperture {self._aperturename}'
+ )
+ _log.debug(debug_message)
if 'IFU' in self.aperturename:
self._mode = 'IFU'
@@ -3259,14 +3309,17 @@ def aperturename(self, value):
self.disperser = 'PRISM' # Set some default spectral mode
self.filter = 'CLEAR'
if self.image_mask not in ['IFU', None]:
- _log.info("The currently-selected image mask (slit) is not compatible with IFU mode. Setting image_mask=None")
+ info_message = (
+ 'The currently-selected image mask (slit) is not compatible with IFU mode. '
+ 'Setting image_mask=None'
+ )
+ _log.info(info_message)
self.image_mask = None
else:
- self._mode = 'imaging' # More to implement here later!
-
+ self._mode = 'imaging' # More to implement here later!
def _tel_coords(self):
- """ Convert from science frame coordinates to telescope frame coordinates using
+ """Convert from science frame coordinates to telescope frame coordinates using
SIAF transformations. Returns (V2, V3) tuple, in arcminutes.
Note that the astropy.units framework is used to return the result as a
@@ -3275,11 +3328,14 @@ def _tel_coords(self):
Some extra steps for NIRSpec to handle the more complicated/flexible mapping between detector and sky coordinates
"""
- if self.aperturename.startswith("NRS_"):
+ if self.aperturename.startswith('NRS_'):
# These apertures don't map directly to particular detector position in the usual way
# Return coords for center of the aperture reference location
- return np.asarray((self._detector_geom_info.aperture.V2Ref,
- self._detector_geom_info.aperture.V3Ref)) / 60 * units.arcmin
+ return (
+ np.asarray((self._detector_geom_info.aperture.V2Ref, self._detector_geom_info.aperture.V3Ref))
+ / 60
+ * units.arcmin
+ )
else:
return super()._tel_coords()
@@ -3296,7 +3352,7 @@ def disperser(self):
Only applies for IFU mode sims, currently; used to help set the
wavelength range to simulate
"""
- if self.mode =='IFU':
+ if self.mode == 'IFU':
return self._disperser
else:
return None
@@ -3309,25 +3365,24 @@ def disperser(self, value):
raise RuntimeError(f'Not a valid NIRSpec disperser name: {value}')
def _calc_psf_format_output(self, result, options):
- """ Format output HDUList. In particular, add some extra metadata if in IFU mode"""
+ """Format output HDUList. In particular, add some extra metadata if in IFU mode"""
super()._calc_psf_format_output(result, options)
if self.mode == 'IFU':
n_exts = len(result)
for ext in np.arange(n_exts):
- result[ext].header['MODE'] = ("IFU", 'This is a NIRSpec IFU mode simulation')
+ result[ext].header['MODE'] = ('IFU', 'This is a NIRSpec IFU mode simulation')
result[ext].header['GRATING'] = (self.disperser, 'Name of the grating (or prism) element simulated.')
-
@property
def band(self):
if self.mode != 'IFU':
return None
- return self.disperser + "/" + self.filter
+ return self.disperser + '/' + self.filter
@band.setter
def band(self, value):
- raise RuntimeError("This is a read-only property. Set grating and/or filter attributes instead.")
+ raise RuntimeError('This is a read-only property. Set grating and/or filter attributes instead.')
class NIRISS(JWInstrument):