From d0291697989d9fbef35457c84925a370f6b013cf Mon Sep 17 00:00:00 2001 From: Bradley Sappington Date: Tue, 21 May 2024 12:26:32 -0400 Subject: [PATCH 1/7] Additional Pep8speaks updates --- dev_utils/compute_psf_library.py | 2 +- dev_utils/field_dependence/basis.py | 10 +- .../field_dependence/read_codev_dat_fits.py | 2 +- dev_utils/wfe_benchmark.py | 10 +- docs/conf.py | 2 +- docs/exts/numfig.py | 10 +- webbpsf/constants.py | 2 +- webbpsf/detectors.py | 76 +++--- webbpsf/gridded_library.py | 1 - webbpsf/mast_wss.py | 25 +- webbpsf/match_data.py | 4 +- webbpsf/opds.py | 25 +- webbpsf/optical_budget.py | 2 +- webbpsf/optics.py | 71 ++--- webbpsf/roman.py | 3 +- webbpsf/tests/test_errorhandling.py | 1 + webbpsf/tests/test_fgs.py | 8 +- webbpsf/tests/test_miri.py | 46 ++-- webbpsf/tests/test_nircam.py | 18 +- webbpsf/tests/test_niriss.py | 11 +- webbpsf/tests/test_nirspec.py | 11 +- webbpsf/tests/test_opds.py | 6 +- webbpsf/tests/test_utils.py | 1 + webbpsf/tests/test_webbpsf.py | 3 +- webbpsf/trending.py | 44 ++-- webbpsf/utils.py | 14 +- webbpsf/webbpsf_core.py | 249 ++++++++++-------- 27 files changed, 345 insertions(+), 312 deletions(-) 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..3dbc2157 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 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..a4e02624 100644 --- a/dev_utils/field_dependence/read_codev_dat_fits.py +++ b/dev_utils/field_dependence/read_codev_dat_fits.py @@ -45,7 +45,7 @@ def main(): ] # Zernike fitting order for each instrument - order= {'fgs': 15, + order = {'fgs': 15, 'nircam': 15, 'miri': 15, 'nirspec': 15, 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..53c61de7 100644 --- a/docs/exts/numfig.py +++ b/docs/exts/numfig.py @@ -2,10 +2,10 @@ from sphinx.roles import XRefRole # Element classes - class page_ref(reference): pass + class num_ref(reference): pass @@ -15,10 +15,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 +59,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 +76,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 = '%s' % (link, labelfmt %(figids[target])) + html = '%s' % (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..b8117dc8 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): @@ -284,8 +285,9 @@ 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') +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 +312,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 +332,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 # 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 +378,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 +494,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 +520,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..34bcef1d 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,12 +103,14 @@ 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.' + 'Cannot find ANY OPDs in MAST within a week before/after that date. \ + Date is likely outside the range of valid data.' ) elif max(obs_table['date_obs_mjd']) < date.mjd: # if len(obs_table) == 1 : #and min(obs_table['date_obs_mjd']) < date.mjd: @@ -222,7 +224,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 +233,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}, \ + 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 +313,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 +506,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 +705,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 \ + {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..cd8d43eb 100644 --- a/webbpsf/opds.py +++ b/webbpsf/opds.py @@ -153,7 +153,8 @@ 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})' + f'The shape of the segment mask file {self._segment_masks.shape} \ + does not match the shape expect: ({self.npix}, {self.npix})' ) self._segment_masks_version = fits.getheader(full_seg_mask_file)['VERSION'] @@ -246,7 +247,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, @@ -851,7 +851,7 @@ def _move(self, segment, type='tilt', vector=None, display=False, return_zernike if type == 'tilt': local_vector[ 2 - ] /= 1000 # convert Z tilt to milliradians instead of microradians because that is what the sensitivity tables use + ] /= 1000 # convert Z tilt to milliradians instead of microradians, that is what the sensitivity tables use units = 'microradians for tip/tilt, milliradians for clocking' else: units = 'microns' @@ -1133,7 +1133,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 +1167,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:]: @@ -1747,7 +1749,10 @@ def _get_zernikes_for_ote_field_dep( 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.' + 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.' ) # Get value of Legendre Polynomials at desired field point. Need to implement model in G. Brady's prototype @@ -2879,12 +2884,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 @@ -3289,7 +3290,9 @@ 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' + 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' ) else: return np.zeros((npix, npix), float) 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..7744d5f1 100644 --- a/webbpsf/optics.py +++ b/webbpsf/optics.py @@ -17,7 +17,6 @@ _log = logging.getLogger('webbpsf') - ####### Classes for modeling aspects of JWST's segmented active primary ##### @@ -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) @@ -547,7 +548,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 +909,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) @@ -2029,7 +2008,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..2c32a79f 100644 --- a/webbpsf/roman.py +++ b/webbpsf/roman.py @@ -313,7 +313,8 @@ def calc_psf( 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.' + 'Geometric distortions are not implemented in WebbPSF for Roman CGI. \ + The add_distortion keyword must be set to False for this case.' ) # Run poppy 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 dc898d74..7451c93c 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 @@ -636,7 +644,7 @@ 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' diff --git a/webbpsf/tests/test_niriss.py b/webbpsf/tests/test_niriss.py index 01dc487b..4c4323cb 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,22 @@ # ------------------ 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..9718b6dd 100644 --- a/webbpsf/tests/test_opds.py +++ b/webbpsf/tests/test_opds.py @@ -393,7 +393,8 @@ def test_apply_field_dependence_model(): # outputs of this model 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)" + ), "Field-dependent OTE WFE at selected field point (NIRISS center) \ + didn't match expected value (test case: explicit call, assume_si_focus=False)" # 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 @@ -408,7 +409,8 @@ def test_apply_field_dependence_model(): rms4 = rms(opd_nis_cen_v2, mask) 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." + ), "Field-dependent OTE WFE at selected field point (NIRISS center) \ + didn't match expected value(test case: implicit call, assume_si_focus=True)" 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..8b12641b 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 = {} @@ -1691,7 +1691,8 @@ def plot_wfs_obs_delta(fn1, fn2, vmax_fraction=1.0, download_opds=True): axes[0, 2].set_title('Difference\n ', fontsize=18) fig.suptitle( - f"{hdul1[0].header['CORR_ID']}, {hdul1[0].header['TSTAMP'][:-3]} vs. {hdul2[0].header['CORR_ID']}, {hdul2[0].header['TSTAMP'][:-3]}", + f"{hdul1[0].header['CORR_ID']}, {hdul1[0].header['TSTAMP'][:-3]} vs. \ + {hdul2[0].header['CORR_ID']}, {hdul2[0].header['TSTAMP'][:-3]}", fontsize=20, fontweight='bold', ) @@ -2017,7 +2018,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 +2097,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 +2123,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} \ + (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 +2135,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 +2168,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..faa9fe9c 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 @@ -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( @@ -729,7 +730,7 @@ def psf_grid( 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 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) @@ -979,11 +980,13 @@ def aperturename(self, value): _log.debug( f'Pixelscale updated to {self.pixelscale} based on average X+Y SciScale at SIAF aperture {detector_apername}' ) - elif ap.AperType == 'COMPOUND' and self.name=='MIRI': + 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.') + _log.info( + f'Aperture {value} is of type COMPOUND for MIRI; There do not exist corresponding SIAF apertures, so we ignore setting detector geometry.' + ) # Now apply changes: self._aperturename = value @@ -991,7 +994,7 @@ 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: @@ -1008,7 +1011,7 @@ def aperturename(self, value): # 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' ) @@ -1039,11 +1042,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 +1090,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 +1261,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 @@ -1833,7 +1841,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 +1849,12 @@ 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.info( + f'Performing initial propagation at minimum wavelength {MIN_REF_WAVE*1e6:.2f} microns; \ + minimum set to avoid phase wrap of segment C3 surface.' + ) 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 @@ -1933,21 +1943,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 +1966,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 +1980,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 +1994,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,8 +2020,7 @@ 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 # and is thus flipped in Y relative to the V frame entrance pupil. Therefore flip sign of pupil_shift_y @@ -2044,42 +2050,44 @@ def __init__(self): # 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) @@ -2345,10 +2353,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 +2390,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 +2410,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 +2427,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') @@ -2702,7 +2708,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 +2922,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 +3031,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 +3104,10 @@ 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 +3134,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 +3204,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 +3218,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. @@ -3230,7 +3242,7 @@ 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: @@ -3238,7 +3250,9 @@ def aperturename(self, value): # 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)) + has_custom_pixelscale = self._aperturename and ( + self.pixelscale != self._get_pixelscale_from_apername(detector_apername) + ) # Now apply changes: self._aperturename = value @@ -3247,11 +3261,13 @@ 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}") + _log.debug( + f'Pixelscale updated to {self.pixelscale} based on average X+Y SciScale at SIAF aperture {self._aperturename}' + ) if 'IFU' in self.aperturename: self._mode = 'IFU' @@ -3259,14 +3275,15 @@ 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") + _log.info( + 'The currently-selected image mask (slit) is not compatible with IFU mode. Setting image_mask=None' + ) 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 +3292,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 +3316,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 +3329,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): From 4622d30ecab55574f5483a9f9de768915584479a Mon Sep 17 00:00:00 2001 From: Bradley Sappington Date: Tue, 21 May 2024 12:58:54 -0400 Subject: [PATCH 2/7] additional linting --- dev_utils/field_dependence/basis.py | 2 +- .../field_dependence/read_codev_dat_fits.py | 13 ++-- docs/exts/numfig.py | 1 + webbpsf/detectors.py | 12 +-- webbpsf/optics.py | 13 ++-- webbpsf/tests/test_nircam.py | 6 +- webbpsf/trending.py | 2 +- webbpsf/webbpsf_core.py | 73 ++++++++++++------- 8 files changed, 75 insertions(+), 47 deletions(-) diff --git a/dev_utils/field_dependence/basis.py b/dev_utils/field_dependence/basis.py index 3dbc2157..2c12c2f6 100644 --- a/dev_utils/field_dependence/basis.py +++ b/dev_utils/field_dependence/basis.py @@ -215,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): diff --git a/dev_utils/field_dependence/read_codev_dat_fits.py b/dev_utils/field_dependence/read_codev_dat_fits.py index a4e02624..d79510c2 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/docs/exts/numfig.py b/docs/exts/numfig.py index 53c61de7..b585fb19 100644 --- a/docs/exts/numfig.py +++ b/docs/exts/numfig.py @@ -1,6 +1,7 @@ from docutils.nodes import SkipNode, Text, caption, figure, raw, reference from sphinx.roles import XRefRole + # Element classes class page_ref(reference): pass diff --git a/webbpsf/detectors.py b/webbpsf/detectors.py index b8117dc8..c14e39b0 100644 --- a/webbpsf/detectors.py +++ b/webbpsf/detectors.py @@ -61,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 @@ -168,7 +169,7 @@ 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 + # Cases for which 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 @@ -284,7 +285,8 @@ 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) +# 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') @@ -315,7 +317,7 @@ def _make_miri_scattering_kernel_2d(in_psf, kernel_amp, oversample=1, wavelength 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 @@ -338,7 +340,7 @@ def _make_miri_scattering_kernel_2d(in_psf, kernel_amp, oversample=1, wavelength 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 diff --git a/webbpsf/optics.py b/webbpsf/optics.py index 7744d5f1..ed2e2d18 100644 --- a/webbpsf/optics.py +++ b/webbpsf/optics.py @@ -17,7 +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): @@ -186,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): @@ -442,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. @@ -512,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 ) @@ -1405,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( diff --git a/webbpsf/tests/test_nircam.py b/webbpsf/tests/test_nircam.py index 7451c93c..8843070f 100644 --- a/webbpsf/tests/test_nircam.py +++ b/webbpsf/tests/test_nircam.py @@ -197,7 +197,8 @@ def do_test_nircam_blc(clobber=False, kind='circular', angle=0, save=False, disp # regular propagation rather than semi-analytic. See poppy issue #169 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}' + ), f'Total flux {totflux} is out of tolerance relative to expectations {exp_flux}, \ + for offset={offset}, angle={angle}' # 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)) @@ -326,7 +327,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 """ diff --git a/webbpsf/trending.py b/webbpsf/trending.py index 8b12641b..6905a11e 100644 --- a/webbpsf/trending.py +++ b/webbpsf/trending.py @@ -2018,7 +2018,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""" diff --git a/webbpsf/webbpsf_core.py b/webbpsf/webbpsf_core.py index faa9fe9c..1b02aa55 100644 --- a/webbpsf/webbpsf_core.py +++ b/webbpsf/webbpsf_core.py @@ -302,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 @@ -388,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 @@ -727,7 +727,8 @@ 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 @@ -754,7 +755,7 @@ def psf_grid( return gridmodel -####### JWInstrument classes ##### +# JWInstrument classes ##### @utils.combine_docstrings @@ -985,7 +986,8 @@ def aperturename(self, value): 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.' + f'Aperture {value} is of type COMPOUND for MIRI; \ + There do not exist corresponding SIAF apertures, so we ignore setting detector geometry.' ) # Now apply changes: @@ -1006,9 +1008,13 @@ 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 - # 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') @@ -1028,7 +1034,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} \ + based on average X+Y SciScale at SIAF aperture {self._aperturename}' ) def _tel_coords(self): @@ -1450,7 +1457,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 @@ -1606,7 +1615,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 @@ -1875,7 +1885,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 @@ -1908,8 +1918,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) @@ -2022,7 +2032,8 @@ def __init__(self): # Modes and default SIAF apertures for each 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. @@ -2047,7 +2058,8 @@ 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'} @@ -2211,7 +2223,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) @@ -2604,7 +2617,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, \ + and channel={self.channel}, module={self.module}' ) else: apname = self._detectors[self._detector] @@ -2648,7 +2662,8 @@ def aperturename(self, value): # 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.' + 'Possibly running an old version of pysiaf missing some NIRCam apertures. \ + Continuing with old aperture names.' ) else: return @@ -2658,7 +2673,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) ) @@ -2682,7 +2698,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} \ + based on average X+Y SciScale at SIAF aperture {self._aperturename}' ) @property @@ -3234,7 +3251,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 @@ -3249,7 +3267,8 @@ def aperturename(self, 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 + # 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) ) @@ -3266,7 +3285,8 @@ 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 {self._aperturename}' + f'Pixelscale updated to {self.pixelscale} \ + based on average X+Y SciScale at SIAF aperture {self._aperturename}' ) if 'IFU' in self.aperturename: @@ -3276,7 +3296,8 @@ def aperturename(self, value): 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' + 'The currently-selected image mask (slit) is not compatible with IFU mode. \ + Setting image_mask=None' ) self.image_mask = None else: From 49a38496fac1ffd5d6a043c52250ff56c30a18c3 Mon Sep 17 00:00:00 2001 From: Bradley Sappington Date: Tue, 21 May 2024 13:01:39 -0400 Subject: [PATCH 3/7] formatting --- dev_utils/field_dependence/read_codev_dat_fits.py | 2 +- webbpsf/webbpsf_core.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/dev_utils/field_dependence/read_codev_dat_fits.py b/dev_utils/field_dependence/read_codev_dat_fits.py index d79510c2..05eedc6f 100644 --- a/dev_utils/field_dependence/read_codev_dat_fits.py +++ b/dev_utils/field_dependence/read_codev_dat_fits.py @@ -51,7 +51,7 @@ def main(): '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/webbpsf/webbpsf_core.py b/webbpsf/webbpsf_core.py index 1b02aa55..b459e55c 100644 --- a/webbpsf/webbpsf_core.py +++ b/webbpsf/webbpsf_core.py @@ -979,7 +979,8 @@ 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}' + f'Pixelscale updated to {self.pixelscale} \ + based on average X+Y SciScale at SIAF aperture {detector_apername}' ) elif ap.AperType == 'COMPOUND' and self.name == 'MIRI': # For MIRI, many of the relevant IFU apertures are of COMPOUND type. From 32e4f6bfc553a5effbac3356949f48ff829c8832 Mon Sep 17 00:00:00 2001 From: Bradley Sappington Date: Tue, 21 May 2024 13:05:13 -0400 Subject: [PATCH 4/7] Future Commits will only evaluate the updated code --- .pep8speaks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 43ac26692d9ad0f210274f34fb9a0ecee8e16454 Mon Sep 17 00:00:00 2001 From: Bradley Sappington Date: Wed, 22 May 2024 10:56:42 -0400 Subject: [PATCH 5/7] string formatting bug fix --- webbpsf/detectors.py | 3 ++- webbpsf/mast_wss.py | 20 ++++++++--------- webbpsf/opds.py | 17 +++++++------- webbpsf/roman.py | 4 ++-- webbpsf/tests/test_opds.py | 3 +-- webbpsf/webbpsf_core.py | 46 +++++++++++++++++++++----------------- 6 files changed, 50 insertions(+), 43 deletions(-) diff --git a/webbpsf/detectors.py b/webbpsf/detectors.py index c14e39b0..a3a1e973 100644 --- a/webbpsf/detectors.py +++ b/webbpsf/detectors.py @@ -169,7 +169,8 @@ def apply_detector_ipc(psf_hdulist, extname='DET_DIST'): """ - # Cases for which 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 diff --git a/webbpsf/mast_wss.py b/webbpsf/mast_wss.py index 34bcef1d..b109d204 100644 --- a/webbpsf/mast_wss.py +++ b/webbpsf/mast_wss.py @@ -103,14 +103,14 @@ 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.' + 'Cannot find ANY OPDs in MAST within a week before/after that date.', + 'Date is likely outside the range of valid data.' ) elif max(obs_table['date_obs_mjd']) < date.mjd: # if len(obs_table) == 1 : #and min(obs_table['date_obs_mjd']) < date.mjd: @@ -233,8 +233,8 @@ 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) @@ -506,8 +506,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: @@ -705,8 +705,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/opds.py b/webbpsf/opds.py index cd8d43eb..e6f02d9c 100644 --- a/webbpsf/opds.py +++ b/webbpsf/opds.py @@ -849,9 +849,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, that is what the sensitivity tables use + ] /= 1000 units = 'microradians for tip/tilt, milliradians for clocking' else: units = 'microns' @@ -1749,10 +1750,10 @@ def _get_zernikes_for_ote_field_dep( 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.' + 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.' ) # Get value of Legendre Polynomials at desired field point. Need to implement model in G. Brady's prototype @@ -3290,9 +3291,9 @@ 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' + 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' ) else: return np.zeros((npix, npix), float) diff --git a/webbpsf/roman.py b/webbpsf/roman.py index 2c32a79f..0078e791 100644 --- a/webbpsf/roman.py +++ b/webbpsf/roman.py @@ -313,8 +313,8 @@ def calc_psf( 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.' + 'Geometric distortions are not implemented in WebbPSF for Roman CGI.', + 'The add_distortion keyword must be set to False for this case.' ) # Run poppy calc_psf diff --git a/webbpsf/tests/test_opds.py b/webbpsf/tests/test_opds.py index 9718b6dd..6895141f 100644 --- a/webbpsf/tests/test_opds.py +++ b/webbpsf/tests/test_opds.py @@ -409,8 +409,7 @@ def test_apply_field_dependence_model(): rms4 = rms(opd_nis_cen_v2, mask) 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)" + ), "Field-dependent OTE WFE at selected field point (NIRISS center) didn't match expected value(test case: implicit call, assume_si_focus=True)" # noqa def test_get_zernike_coeffs_from_smif(): diff --git a/webbpsf/webbpsf_core.py b/webbpsf/webbpsf_core.py index b459e55c..a9486eed 100644 --- a/webbpsf/webbpsf_core.py +++ b/webbpsf/webbpsf_core.py @@ -979,16 +979,16 @@ 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}' + 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': # 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.' + f'Aperture {value} is of type COMPOUND for MIRI;', + 'There do not exist corresponding SIAF apertures, so we ignore setting detector geometry.' ) # Now apply changes: @@ -1035,8 +1035,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): @@ -1861,8 +1861,8 @@ def calc_datacube_fast(self, wavelengths, compare_methods=False, outfile=None, * 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.' + f'Performing initial propagation at minimum wavelength {MIN_REF_WAVE*1e6:.2f} microns;', + 'minimum set to avoid phase wrap of segment C3 surface.' ) else: _log.info(f'Performing initial propagation at average wavelength {ref_wave*1e6:.2f} microns.') @@ -2031,7 +2031,10 @@ 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 @@ -2618,8 +2621,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] @@ -2663,8 +2666,8 @@ def aperturename(self, value): # 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.' + 'Possibly running an old version of pysiaf missing some NIRCam apertures.', + 'Continuing with old aperture names.' ) else: return @@ -2699,8 +2702,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}' ) @property @@ -3125,7 +3128,10 @@ 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': '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 @@ -3286,8 +3292,8 @@ 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 {self._aperturename}' + f'Pixelscale updated to {self.pixelscale}', + f'based on average X+Y SciScale at SIAF aperture {self._aperturename}' ) if 'IFU' in self.aperturename: @@ -3297,8 +3303,8 @@ def aperturename(self, value): 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' + 'The currently-selected image mask (slit) is not compatible with IFU mode.', + 'Setting image_mask=None' ) self.image_mask = None else: From 8d81d3ed1287b62128b6c38b15f9234843a021be Mon Sep 17 00:00:00 2001 From: Bradley Sappington Date: Wed, 22 May 2024 11:03:02 -0400 Subject: [PATCH 6/7] hanging indent fix --- webbpsf/opds.py | 6 +++--- webbpsf/webbpsf_core.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/webbpsf/opds.py b/webbpsf/opds.py index e6f02d9c..86fe13b9 100644 --- a/webbpsf/opds.py +++ b/webbpsf/opds.py @@ -1751,7 +1751,7 @@ def _get_zernikes_for_ote_field_dep( # 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', + '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.' ) @@ -3292,8 +3292,8 @@ def sur_to_opd(sur_filename, ignore_missing=False, npix=256): 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' + 'for now, manually retrieve from WSSTAS at', + 'https://wsstas.stsci.edu/wsstas/staticPage/showContent/RecentSURs?primary=master.png' ) else: return np.zeros((npix, npix), float) diff --git a/webbpsf/webbpsf_core.py b/webbpsf/webbpsf_core.py index a9486eed..ef701016 100644 --- a/webbpsf/webbpsf_core.py +++ b/webbpsf/webbpsf_core.py @@ -988,7 +988,7 @@ def aperturename(self, value): # 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.' + 'There do not exist corresponding SIAF apertures, so we ignore setting detector geometry.' ) # Now apply changes: @@ -1862,7 +1862,7 @@ def calc_datacube_fast(self, wavelengths, compare_methods=False, outfile=None, * 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.' + 'minimum set to avoid phase wrap of segment C3 surface.' ) else: _log.info(f'Performing initial propagation at average wavelength {ref_wave*1e6:.2f} microns.') From 3f916c9fbcb6a3031ee657ea89cc55405dc8ca67 Mon Sep 17 00:00:00 2001 From: Bradley Sappington Date: Wed, 22 May 2024 12:40:12 -0400 Subject: [PATCH 7/7] string formatting updates --- webbpsf/mast_wss.py | 5 +++-- webbpsf/opds.py | 23 ++++++++++++---------- webbpsf/roman.py | 5 +++-- webbpsf/tests/test_nircam.py | 7 +++++-- webbpsf/tests/test_niriss.py | 6 ++++-- webbpsf/tests/test_opds.py | 13 +++++++++--- webbpsf/trending.py | 11 +++++++---- webbpsf/webbpsf_core.py | 38 ++++++++++++++++++++++-------------- 8 files changed, 68 insertions(+), 40 deletions(-) diff --git a/webbpsf/mast_wss.py b/webbpsf/mast_wss.py index b109d204..7ac1e992 100644 --- a/webbpsf/mast_wss.py +++ b/webbpsf/mast_wss.py @@ -108,10 +108,11 @@ def mast_wss_opds_around_date_query(date, verbose=True): ) if len(obs_table) == 0: - raise RuntimeError( - 'Cannot find ANY OPDs in MAST within a week before/after that date.', + 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: diff --git a/webbpsf/opds.py b/webbpsf/opds.py index 86fe13b9..fbc77fad 100644 --- a/webbpsf/opds.py +++ b/webbpsf/opds.py @@ -152,10 +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'] @@ -1749,12 +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', - f'{instrument}: {min_x_field}-{max_x_field}, {min_y_field}-{max_y_field}.', + 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 @@ -3290,11 +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', + 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/roman.py b/webbpsf/roman.py index 0078e791..93ebe0f9 100644 --- a/webbpsf/roman.py +++ b/webbpsf/roman.py @@ -312,10 +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.', + 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_nircam.py b/webbpsf/tests/test_nircam.py index 8843070f..9ea1c865 100644 --- a/webbpsf/tests/test_nircam.py +++ b/webbpsf/tests/test_nircam.py @@ -195,10 +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)) diff --git a/webbpsf/tests/test_niriss.py b/webbpsf/tests/test_niriss.py index 4c4323cb..19979683 100644 --- a/webbpsf/tests/test_niriss.py +++ b/webbpsf/tests/test_niriss.py @@ -19,8 +19,10 @@ def test_niriss_source_offset_45(): 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_opds.py b/webbpsf/tests/test_opds.py index 6895141f..368263ca 100644 --- a/webbpsf/tests/test_opds.py +++ b/webbpsf/tests/test_opds.py @@ -391,10 +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 @@ -407,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)" # noqa + ), assert_message def test_get_zernike_coeffs_from_smif(): diff --git a/webbpsf/trending.py b/webbpsf/trending.py index 6905a11e..febcd6dc 100644 --- a/webbpsf/trending.py +++ b/webbpsf/trending.py @@ -1690,9 +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', ) @@ -2123,8 +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 diff --git a/webbpsf/webbpsf_core.py b/webbpsf/webbpsf_core.py index ef701016..8817aa1e 100644 --- a/webbpsf/webbpsf_core.py +++ b/webbpsf/webbpsf_core.py @@ -978,18 +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}', + debug_message = ( + f'Pixelscale updated to {self.pixelscale} ' f'based on average X+Y SciScale at SIAF aperture {detector_apername}' ) + _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;', + 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 @@ -1001,10 +1003,11 @@ def aperturename(self, 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, @@ -1860,10 +1863,11 @@ 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;', + 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.') @@ -2665,10 +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.', + warning_message = ( + 'Possibly running an old version of pysiaf missing some NIRCam apertures. ' 'Continuing with old aperture names.' ) + _log.warning(warning_message) else: return @@ -2701,10 +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}', + 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): @@ -3291,10 +3297,11 @@ 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}', + 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' @@ -3302,10 +3309,11 @@ 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.', + 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!