From 4e6bdda615ae1c8c742286ace94bd96d9f9de95b Mon Sep 17 00:00:00 2001 From: obiwan76 Date: Mon, 13 Nov 2023 09:45:52 -0500 Subject: [PATCH 01/12] combining issues 726 and 736 --- webbpsf/detectors.py | 2 ++ webbpsf/gridded_library.py | 7 ++----- webbpsf/webbpsf_core.py | 13 +++++++++---- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/webbpsf/detectors.py b/webbpsf/detectors.py index 10e174e2..fd61b0e1 100644 --- a/webbpsf/detectors.py +++ b/webbpsf/detectors.py @@ -166,6 +166,8 @@ def apply_detector_ipc(psf_hdulist, extname = 'DET_DIST'): """ # In cases for which the user has asked for the IPC to be applied to a not-present extension, we have nothing to add this to + + if psf_hdulist is None: return 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/gridded_library.py b/webbpsf/gridded_library.py index 48ed4087..fd8ed6db 100644 --- a/webbpsf/gridded_library.py +++ b/webbpsf/gridded_library.py @@ -302,10 +302,7 @@ def create_grid(self): psf[ext].data = astropy.convolution.convolve(psf[ext].data, kernel) # Convolve PSF with a model for interpixel capacitance - # note, normally this is applied in calc_psf to the detector-sampled data; - # here we specially apply this to the oversampled data - if self.add_distortion and self.webb.options.get('add_ipc', True): - webbpsf.detectors.apply_detector_ipc(psf, extname=ext) + # This is apply outside the gridded libary see issue #736 # Add PSF to 5D array psf_arr[i, :, :] = psf[ext].data @@ -390,7 +387,7 @@ def create_grid(self): # copy all the jitter-related keys (the exact set of keywords varies based on jitter type) # Also copy charge diffusion and IPC for k in psf[ext].header.keys(): # do the rest - if k.startswith('JITR') or k.startswith('IPC') or k.startswith('CHDF'): + if k.startswith('JITR') or k.startswith('IPC') or k.startswith('PPC') or k.startswith('CHDF'): meta[k] = (psf[ext].header[k], psf[ext].header.comments[k]) meta["DATE"] = (psf[ext].header["DATE"], "Date of calculation") diff --git a/webbpsf/webbpsf_core.py b/webbpsf/webbpsf_core.py index 57d3e2fa..2c2a5010 100644 --- a/webbpsf/webbpsf_core.py +++ b/webbpsf/webbpsf_core.py @@ -1118,6 +1118,8 @@ def _calc_psf_format_output(self, result, options): # Pull values from options dictionary add_distortion = options.get('add_distortion', True) crop_psf = options.get('crop_psf', True) + # you can turn on/off IPC corrections via the add_ipc option, default True. + add_ipc = options.get('add_ipc', True) # Add distortion if set in calc_psf if add_distortion: @@ -1141,6 +1143,7 @@ def _calc_psf_format_output(self, result, options): psf_rotated = distortion.apply_rotation(result, crop=crop_psf) # apply rotation psf_siaf_distorted = distortion.apply_distortion(psf_rotated) # apply siaf distortion model psf_distorted = detectors.apply_detector_charge_diffusion(psf_siaf_distorted, options) # apply detector charge transfer model + print('NIRCam Detector distortion inside') elif self.name == "MIRI": # Apply distortion effects to MIRI psf: Distortion and MIRI Scattering _log.debug("MIRI: Adding optical distortion and Si:As detector internal scattering") @@ -1160,17 +1163,19 @@ def _calc_psf_format_output(self, result, options): # Edit the variable to match if input didn't request distortion # (cannot set result = psf_distorted due to return method) + [result.append(fits.ImageHDU()) for i in np.arange(len(psf_distorted) - len(result))] for ext in np.arange(len(psf_distorted)): result[ext] = psf_distorted[ext] + # Rewrite result variable based on output_mode; this includes binning down to detector sampling. SpaceTelescopeInstrument._calc_psf_format_output(self, result, options) - # you can turn on/off IPC corrections via the add_ipc option, default True. - add_ipc = options.get('add_ipc', True) - if add_ipc and add_distortion: - result = detectors.apply_detector_ipc(result) # apply detector IPC model (after binning to detector sampling) + if add_ipc and add_distortion and ('DET_DIST' in result): + result = detectors.apply_detector_ipc(result) # apply detector IPC model (after binning to detector sampling) + if add_ipc and add_distortion and ('OVERDIST' in result): + result = detectors.apply_detector_ipc(result, extname = 'OVERDIST') # apply detector IPC model to oversampled PSF def interpolate_was_opd(self, array, newdim): """ Interpolates an input 2D array to any given size. From b8eb69f3e27d5b41a6c233ce37ced82154a0be76 Mon Sep 17 00:00:00 2001 From: obiwan76 Date: Mon, 13 Nov 2023 10:39:19 -0500 Subject: [PATCH 02/12] removing some print statement used for testing --- webbpsf/webbpsf_core.py | 1 - 1 file changed, 1 deletion(-) diff --git a/webbpsf/webbpsf_core.py b/webbpsf/webbpsf_core.py index 2c2a5010..52924c25 100644 --- a/webbpsf/webbpsf_core.py +++ b/webbpsf/webbpsf_core.py @@ -1143,7 +1143,6 @@ def _calc_psf_format_output(self, result, options): psf_rotated = distortion.apply_rotation(result, crop=crop_psf) # apply rotation psf_siaf_distorted = distortion.apply_distortion(psf_rotated) # apply siaf distortion model psf_distorted = detectors.apply_detector_charge_diffusion(psf_siaf_distorted, options) # apply detector charge transfer model - print('NIRCam Detector distortion inside') elif self.name == "MIRI": # Apply distortion effects to MIRI psf: Distortion and MIRI Scattering _log.debug("MIRI: Adding optical distortion and Si:As detector internal scattering") From dd36f2852af7c449c70fdf1a525634df280f6067 Mon Sep 17 00:00:00 2001 From: obiwan76 Date: Wed, 6 Dec 2023 15:24:10 -0500 Subject: [PATCH 03/12] adding match data changes --- webbpsf/match_data.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/webbpsf/match_data.py b/webbpsf/match_data.py index cbf68961..4d42530f 100644 --- a/webbpsf/match_data.py +++ b/webbpsf/match_data.py @@ -48,10 +48,20 @@ def setup_sim_to_match_file(filename_or_HDUList, verbose=True, plot=False, choic # per-instrument specializations if inst.name == 'NIRCam': if header['PUPIL'].startswith('MASK'): - inst.pupil_mask = header['PUPIL'] + if header['PUPIL'] == 'MASKBAR': + inst.pupil_mask = header['CORONMSK'].replace('MASKA', 'MASK') + else: + inst.pupil_mask = header['PUPIL'] inst.image_mask = header['CORONMSK'].replace('MASKA', 'MASK') # note, have to modify the value slightly for # consistency with the labels used in webbpsf + inst.set_position_from_aperture_name(header['APERNAME']) # Redo this, in case the image_mask setting auto switched it to something else + elif header['PUPIL'].startswith('F'): + inst.filter = header['PUPIL'] + else: + inst.pupil_mask = header['PUPIL'] + + elif inst.name == 'MIRI': if inst.filter in ['F1065C', 'F1140C', 'F1550C']: inst.image_mask = 'FQPM'+inst.filter[1:5] From fe4fb5479fb053a3386632a9eb24bc81e075ec98 Mon Sep 17 00:00:00 2001 From: Marshall Perrin Date: Fri, 8 Dec 2023 13:57:51 -0500 Subject: [PATCH 04/12] use SIAF aperture name, possibly from match-data, to set offset along NIRCam coron bar mask --- webbpsf/webbpsf_core.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/webbpsf/webbpsf_core.py b/webbpsf/webbpsf_core.py index 52924c25..c3972f19 100644 --- a/webbpsf/webbpsf_core.py +++ b/webbpsf/webbpsf_core.py @@ -2438,13 +2438,22 @@ def _addAdditionalOptics(self, optsys, oversample=2): SAM_box_size = 5.0 elif ((self.image_mask == 'MASKSWB') or (self.image_mask == 'MASKLWB')): bar_offset = self.options.get('bar_offset', None) - # If the bar offset is not provided, use the filter name to lookup the default + # If the bar offset is not provided, use the SIAF aperture name, or else the filter name to lookup the default # position. If an offset is provided and is a floating point value, use that # directly as the offset. Otherwise assume it's a filter name and try passing # that in to the auto offset. (that allows for selecting the narrow position, or # for simulating using a given filter at some other filter's position.) if bar_offset is None: - auto_offset = self.filter + # Try to use the SIAF aperture name to determine the offset + # This can help better automate simulations matching data, since match_data.py will + # have copied the aperturename from the header, like NRCA5_MASKLWB_NARROW or similar + if 'MASK' in self.aperturename: + try: + auto_offset = self.aperturename.split('_')[-1].replace('NARROW', 'narrow') # set to lower case for consistency with existing code in optics.py + _log.info(f"Set bar offset to {auto_offset} based on current aperture name {self.aperturename}") + except: + auto_offset = self.filter + _log.info(f"Set bar offset to {auto_offset} based on current filter {self.filter}") else: try: _ = float(bar_offset) From 1370af7567cb10ce904197592975ff32ecefc7ba Mon Sep 17 00:00:00 2001 From: Marshall Perrin Date: Fri, 8 Dec 2023 15:00:39 -0500 Subject: [PATCH 05/12] incorporate get_coron_apname from webbpsf_ext, courtesy of @JarronL --- webbpsf/match_data.py | 124 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 123 insertions(+), 1 deletion(-) diff --git a/webbpsf/match_data.py b/webbpsf/match_data.py index 4d42530f..e8d6dba1 100644 --- a/webbpsf/match_data.py +++ b/webbpsf/match_data.py @@ -6,6 +6,7 @@ import scipy.optimize import webbpsf +import pysiaf def setup_sim_to_match_file(filename_or_HDUList, verbose=True, plot=False, choice='closest'): @@ -55,7 +56,12 @@ def setup_sim_to_match_file(filename_or_HDUList, verbose=True, plot=False, choic inst.image_mask = header['CORONMSK'].replace('MASKA', 'MASK') # note, have to modify the value slightly for # consistency with the labels used in webbpsf - inst.set_position_from_aperture_name(header['APERNAME']) # Redo this, in case the image_mask setting auto switched it to something else + # The apername keyword is not always correct for cases with dual-channel coronagraphy + # in some such cases, APERNAME != PPS_APER. Let's ensure we have the proper apername for this channel: + apername = get_coron_apname(header) + inst.set_position_from_aperture_name(apername) + + elif header['PUPIL'].startswith('F'): inst.filter = header['PUPIL'] else: @@ -88,3 +94,119 @@ def setup_sim_to_match_file(filename_or_HDUList, verbose=True, plot=False, choic return inst + + +def get_coron_apname(input): + """Get aperture name from header or data model + + Handles edge cases for dual-channel coronagraphy. + + By Jarron Leisenring originally in webbpsf_ext, copied here by permission + + Parameters + ========== + input : fits.header.Header or datamodels.DataModel + Input header or data model + """ + + if isinstance(input, (fits.header.Header)): + # Aperture names + apname = input['APERNAME'] + apname_pps = input['PPS_APER'] + subarray = input['SUBARRAY'] + else: + # Data model meta info + meta = input.meta + + # Aperture names + apname = meta.aperture.name + apname_pps = meta.aperture.pps_name + subarray = meta.subarray.name + + # print(apname, apname_pps, subarray) + + # No need to do anything if the aperture names are the same + # Also skip if MASK not in apname_pps + if ((apname==apname_pps) or ('MASK' not in apname_pps)) and ('400X256' not in subarray): + apname_new = apname + else: + # Should only get here if coron mask and apname doesn't match PPS + apname_str_split = apname.split('_') + sca = apname_str_split[0] + image_mask = get_mask_from_pps(apname_pps) + + # Get subarray info + # Sometimes apname erroneously has 'FULL' in it + # So, first for subarray info in apname_pps + if ('400X256' in apname_pps) or ('400X256' in subarray): + apn0 = f'{sca}_400X256' + elif ('FULL' in apname_pps): + apn0 = f'{sca}_FULL' + else: + apn0 = sca + + apname_new = f'{apn0}_{image_mask}' + + # Append filter or NARROW if needed + pps_str_arr = apname_pps.split('_') + last_str = pps_str_arr[-1] + # Look for filter specified in PPS aperture name + if ('_F1' in apname_pps) or ('_F2' in apname_pps) or ('_F3' in apname_pps) or ('_F4' in apname_pps): + # 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:] + apname_new += f'_{filter}' + elif last_str=='NARROW': + apname_new += '_NARROW' + elif ('TAMASK' in apname_pps) and ('WB' in apname_pps[-1]): + apname_new += '_WEDGE_BAR' + elif ('TAMASK' in apname_pps) and (apname_pps[-1]=='R'): + apname_new += '_WEDGE_RND' + + # print(apname_new) + + # If apname_new doesn't exit, we need to fall back to apname + # even if it may not fully make sense. + if apname_new in pysiaf.Siaf('NIRCam').apernames: + return apname_new + else: + return apname + + +def get_mask_from_pps(apname_pps): + """Get mask name from PPS aperture name + + The PPS aperture name is of the form: + NRC[A/B][1-5]_[FULL]_[MASK] + where MASK is the name of the coronagraphic mask used. + + For target acquisition apertures the mask name can be + prependend with "TA" (eg., TAMASK335R). + + Return '' if MASK not in input aperture name. + """ + + if 'MASK' not in apname_pps: + return '' + + pps_str_arr = apname_pps.split('_') + for s in pps_str_arr: + if 'MASK' in s: + image_mask = s + break + + # Special case for TA apertures + if 'TA' in image_mask: + # Remove TA from mask name + image_mask = image_mask.replace('TA', '') + + # Remove FS from mask name + if 'FS' in image_mask: + image_mask = image_mask.replace('FS', '') + + # Remove trailing S or L from LWB and SWB TA apertures + if ('WB' in image_mask) and (image_mask[-1]=='S' or image_mask[-1]=='L'): + image_mask = image_mask[:-1] + + return image_mask From 440f253bbf6d0424c2bb9ff37a701cc7f5ebe0f8 Mon Sep 17 00:00:00 2001 From: obiwan76 Date: Fri, 26 Jan 2024 16:45:48 -0500 Subject: [PATCH 06/12] fixed a problem about applying IPC effects twice when using the psf cal path inside the gridded library --- webbpsf/detectors.py | 9 +++++++-- webbpsf/gridded_library.py | 12 +++++++++++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/webbpsf/detectors.py b/webbpsf/detectors.py index fd61b0e1..4b9cdf42 100644 --- a/webbpsf/detectors.py +++ b/webbpsf/detectors.py @@ -167,16 +167,21 @@ 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 - if psf_hdulist is None: return + #if psf_hdulist is None: return if extname not in psf_hdulist: webbpsf.webbpsf_core._log.debug(f"Skipping IPC simulation since ext {extname} is not found") return + # This avoid applying IPC corrections twice, especially when calling the psf_grid code path for making ePSFs + # because the IPC corrections are applied in gridded_library + + keyword = 'IPCINST' + if keyword in psf_hdulist[extname].header._keyword_indices: return + inst = psf_hdulist[extname].header['INSTRUME'].upper() oversample = psf_hdulist[extname].header['OVERSAMP'] kernel, meta = get_detector_ipc_model(inst, psf_hdulist[extname].header) - if kernel is not None: if inst.upper()=='NIRCAM': diff --git a/webbpsf/gridded_library.py b/webbpsf/gridded_library.py index fd8ed6db..f360fd62 100644 --- a/webbpsf/gridded_library.py +++ b/webbpsf/gridded_library.py @@ -291,6 +291,13 @@ def create_grid(self): if self.verbose is True: print(" Position {}/{}: {} pixels".format(i+1, len(self.location_list), loc)) + # Deactivate IPC corrections, if any, before calc_psf as we are applying them later + self.webb.options['add_ipc_gridded'] = False # add dictionary key to keep track of the user's IPC input + # Deactivate IPC corrections, if any, before calc_psf as we are applying them later + if self.webb.options.get('add_ipc', True): + self.webb.options['add_ipc'] = False + self.webb.options['add_ipc_gridded'] = True + # Create PSF psf = self.webb.calc_psf(**self._kwargs) if self.verbose is True: @@ -302,7 +309,10 @@ def create_grid(self): psf[ext].data = astropy.convolution.convolve(psf[ext].data, kernel) # Convolve PSF with a model for interpixel capacitance - # This is apply outside the gridded libary see issue #736 + + if self.add_distortion and self.webb.options['add_ipc_gridded']: + webbpsf.detectors.apply_detector_ipc(psf, extname=ext) + self.webb.options['add_ipc'] = True # restore the dictionary keyword for the IPC # Add PSF to 5D array psf_arr[i, :, :] = psf[ext].data From e7055bdf5ba310cb354d92c00d709562afa01217 Mon Sep 17 00:00:00 2001 From: obiwan76 Date: Fri, 26 Jan 2024 16:57:07 -0500 Subject: [PATCH 07/12] minor changes to comments and fixing the detector test --- webbpsf/detectors.py | 4 +--- webbpsf/gridded_library.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/webbpsf/detectors.py b/webbpsf/detectors.py index 4b9cdf42..b59786e4 100644 --- a/webbpsf/detectors.py +++ b/webbpsf/detectors.py @@ -172,9 +172,7 @@ def apply_detector_ipc(psf_hdulist, extname = 'DET_DIST'): webbpsf.webbpsf_core._log.debug(f"Skipping IPC simulation since ext {extname} is not found") return - # This avoid applying IPC corrections twice, especially when calling the psf_grid code path for making ePSFs - # because the IPC corrections are applied in gridded_library - + # This avoid applying IPC corrections twice keyword = 'IPCINST' if keyword in psf_hdulist[extname].header._keyword_indices: return diff --git a/webbpsf/gridded_library.py b/webbpsf/gridded_library.py index f360fd62..8cb83395 100644 --- a/webbpsf/gridded_library.py +++ b/webbpsf/gridded_library.py @@ -291,7 +291,7 @@ def create_grid(self): if self.verbose is True: print(" Position {}/{}: {} pixels".format(i+1, len(self.location_list), loc)) - # Deactivate IPC corrections, if any, before calc_psf as we are applying them later + self.webb.options['add_ipc_gridded'] = False # add dictionary key to keep track of the user's IPC input # Deactivate IPC corrections, if any, before calc_psf as we are applying them later if self.webb.options.get('add_ipc', True): From 7f49965ac43bc2c3d2faf76d278015306a5855eb Mon Sep 17 00:00:00 2001 From: Marshall Perrin Date: Fri, 22 Mar 2024 10:55:15 -0400 Subject: [PATCH 08/12] Rework logic for setting NRC coron bar offset positions from aperturenames. Fixes a test failure. --- webbpsf/webbpsf_core.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/webbpsf/webbpsf_core.py b/webbpsf/webbpsf_core.py index c3972f19..c2711306 100644 --- a/webbpsf/webbpsf_core.py +++ b/webbpsf/webbpsf_core.py @@ -2443,17 +2443,26 @@ def _addAdditionalOptics(self, optsys, oversample=2): # directly as the offset. Otherwise assume it's a filter name and try passing # that in to the auto offset. (that allows for selecting the narrow position, or # for simulating using a given filter at some other filter's position.) + # This code is somewhat convoluted, for historical reasons and back-compatibility if bar_offset is None: # Try to use the SIAF aperture name to determine the offset # This can help better automate simulations matching data, since match_data.py will # have copied the aperturename from the header, like NRCA5_MASKLWB_NARROW or similar if 'MASK' in self.aperturename: - try: - auto_offset = self.aperturename.split('_')[-1].replace('NARROW', 'narrow') # set to lower case for consistency with existing code in optics.py + apname_last_part = self.aperturename.split('_')[-1] + if apname_last_part=='NARROW': + auto_offset = 'narrow' # set to lower case for consistency with existing code in optics.py _log.info(f"Set bar offset to {auto_offset} based on current aperture name {self.aperturename}") - except: + elif apname_last_part.startswith('F'): + auto_offset = apname_last_part + _log.info(f"Set bar offset to {auto_offset} based on current aperture name {self.aperturename}") + else: auto_offset = self.filter _log.info(f"Set bar offset to {auto_offset} based on current filter {self.filter}") + else: + auto_offset = self.filter + _log.info(f"Set bar offset to {auto_offset} based on current filter {self.filter}") + else: try: _ = float(bar_offset) From f7a5ff6c9652e4f488048758a5fde4f530636273 Mon Sep 17 00:00:00 2001 From: Marshall Perrin Date: Fri, 22 Mar 2024 11:19:22 -0400 Subject: [PATCH 09/12] Fix issue #806. Thanks to @JarronL for this code --- webbpsf/match_data.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/webbpsf/match_data.py b/webbpsf/match_data.py index e8d6dba1..51134228 100644 --- a/webbpsf/match_data.py +++ b/webbpsf/match_data.py @@ -38,6 +38,10 @@ def setup_sim_to_match_file(filename_or_HDUList, verbose=True, plot=False, choic # webbpsf doesn't model the MIRI LRS prism spectral response print("Please note, webbpsf does not currently model the LRS spectral response. Setting filter to F770W instead.") inst.filter='F770W' + elif (inst.name == 'NIRCam') and (header['PUPIL'][0] == 'F') and (header['PUPIL'][-1] in ['N', 'M']): + # These NIRCam filters are physically in the pupil wheel, but still act as filters. + # Grab the filter name from the PUPIL keyword in this case. + inst.filter = header['PUPIL'] else: inst.filter=header['filter'] inst.set_position_from_aperture_name(header['APERNAME']) From f53bc64fc96bd1cefc01116fb158027ac3432d99 Mon Sep 17 00:00:00 2001 From: Marshall Perrin Date: Mon, 1 Apr 2024 10:48:24 -0400 Subject: [PATCH 10/12] some code style and internal var names cleanup for clarity --- webbpsf/__init__.py | 2 +- webbpsf/detectors.py | 7 +++---- webbpsf/gridded_library.py | 9 +++------ webbpsf/match_data.py | 19 ++++++++----------- webbpsf/webbpsf_core.py | 2 -- 5 files changed, 15 insertions(+), 24 deletions(-) diff --git a/webbpsf/__init__.py b/webbpsf/__init__.py index acb99bc0..c202f4bb 100644 --- a/webbpsf/__init__.py +++ b/webbpsf/__init__.py @@ -128,4 +128,4 @@ class Conf(_config.ConfigNamespace): from .jupyter_gui import show_notebook_interface -from .match_data import setup_sim_to_match_file \ No newline at end of file +from .match_data import setup_sim_to_match_file diff --git a/webbpsf/detectors.py b/webbpsf/detectors.py index b59786e4..f397725c 100644 --- a/webbpsf/detectors.py +++ b/webbpsf/detectors.py @@ -166,15 +166,14 @@ 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 - - #if psf_hdulist is None: return if extname not in psf_hdulist: webbpsf.webbpsf_core._log.debug(f"Skipping IPC simulation since ext {extname} is not found") return - # This avoid applying IPC corrections twice + # This avoid applying IPC effect simulations twice keyword = 'IPCINST' - if keyword in psf_hdulist[extname].header._keyword_indices: return + if keyword in psf_hdulist[extname].header._keyword_indices: + return inst = psf_hdulist[extname].header['INSTRUME'].upper() oversample = psf_hdulist[extname].header['OVERSAMP'] diff --git a/webbpsf/gridded_library.py b/webbpsf/gridded_library.py index 8cb83395..1fcb6a0e 100644 --- a/webbpsf/gridded_library.py +++ b/webbpsf/gridded_library.py @@ -291,12 +291,10 @@ def create_grid(self): if self.verbose is True: print(" Position {}/{}: {} pixels".format(i+1, len(self.location_list), loc)) - - self.webb.options['add_ipc_gridded'] = False # add dictionary key to keep track of the user's IPC input + add_ipc_gridded = self.webb.options.get('add_ipc', False) # add variable to keep track of the user's IPC input # Deactivate IPC corrections, if any, before calc_psf as we are applying them later if self.webb.options.get('add_ipc', True): self.webb.options['add_ipc'] = False - self.webb.options['add_ipc_gridded'] = True # Create PSF psf = self.webb.calc_psf(**self._kwargs) @@ -309,10 +307,9 @@ def create_grid(self): psf[ext].data = astropy.convolution.convolve(psf[ext].data, kernel) # Convolve PSF with a model for interpixel capacitance - - if self.add_distortion and self.webb.options['add_ipc_gridded']: + if self.add_distortion and add_ipc_gridded: webbpsf.detectors.apply_detector_ipc(psf, extname=ext) - self.webb.options['add_ipc'] = True # restore the dictionary keyword for the IPC + self.webb.options['add_ipc'] = add_ipc_gridded# restore the user's value for the IPC option # Add PSF to 5D array psf_arr[i, :, :] = psf[ext].data diff --git a/webbpsf/match_data.py b/webbpsf/match_data.py index 51134228..4c7aea8e 100644 --- a/webbpsf/match_data.py +++ b/webbpsf/match_data.py @@ -59,19 +59,16 @@ def setup_sim_to_match_file(filename_or_HDUList, verbose=True, plot=False, choic inst.pupil_mask = header['PUPIL'] inst.image_mask = header['CORONMSK'].replace('MASKA', 'MASK') # note, have to modify the value slightly for # consistency with the labels used in webbpsf - # The apername keyword is not always correct for cases with dual-channel coronagraphy # in some such cases, APERNAME != PPS_APER. Let's ensure we have the proper apername for this channel: - apername = get_coron_apname(header) + apername = get_nrc_coron_apname(header) inst.set_position_from_aperture_name(apername) - elif header['PUPIL'].startswith('F'): inst.filter = header['PUPIL'] else: inst.pupil_mask = header['PUPIL'] - elif inst.name == 'MIRI': if inst.filter in ['F1065C', 'F1140C', 'F1550C']: inst.image_mask = 'FQPM'+inst.filter[1:5] @@ -100,8 +97,8 @@ def setup_sim_to_match_file(filename_or_HDUList, verbose=True, plot=False, choic -def get_coron_apname(input): - """Get aperture name from header or data model +def get_nrc_coron_apname(input): + """Get NIRCam coronagraph aperture name from header or data model Handles edge cases for dual-channel coronagraphy. @@ -137,7 +134,7 @@ def get_coron_apname(input): # Should only get here if coron mask and apname doesn't match PPS apname_str_split = apname.split('_') sca = apname_str_split[0] - image_mask = get_mask_from_pps(apname_pps) + image_mask = get_nrc_coron_mask_from_pps_apername(apname_pps) # Get subarray info # Sometimes apname erroneously has 'FULL' in it @@ -178,11 +175,11 @@ def get_coron_apname(input): return apname -def get_mask_from_pps(apname_pps): - """Get mask name from PPS aperture name - +def get_nrc_coron_mask_from_pps_apername(apname_pps): + """Get NIRCam coronagraph mask name from PPS aperture name + The PPS aperture name is of the form: - NRC[A/B][1-5]_[FULL]_[MASK] + NRC[A/B][1-5]_[FULL]_[TA][MASK] where MASK is the name of the coronagraphic mask used. For target acquisition apertures the mask name can be diff --git a/webbpsf/webbpsf_core.py b/webbpsf/webbpsf_core.py index c2711306..824aeda7 100644 --- a/webbpsf/webbpsf_core.py +++ b/webbpsf/webbpsf_core.py @@ -1162,12 +1162,10 @@ def _calc_psf_format_output(self, result, options): # Edit the variable to match if input didn't request distortion # (cannot set result = psf_distorted due to return method) - [result.append(fits.ImageHDU()) for i in np.arange(len(psf_distorted) - len(result))] for ext in np.arange(len(psf_distorted)): result[ext] = psf_distorted[ext] - # Rewrite result variable based on output_mode; this includes binning down to detector sampling. SpaceTelescopeInstrument._calc_psf_format_output(self, result, options) From 54b07043ffe06bb8ddafcf128bb7ce91e3f06e0f Mon Sep 17 00:00:00 2001 From: Marshall Perrin Date: Mon, 1 Apr 2024 11:06:07 -0400 Subject: [PATCH 11/12] add an additional unit test for IPC effect --- webbpsf/tests/test_detectors.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/webbpsf/tests/test_detectors.py b/webbpsf/tests/test_detectors.py index e205ebfb..b82cd042 100644 --- a/webbpsf/tests/test_detectors.py +++ b/webbpsf/tests/test_detectors.py @@ -169,3 +169,27 @@ def test_ipc_oversampling_equivalence(oversamp = 2): psf_detdist_v2 = poppy.utils.rebin_array(testpsf['OVERDIST'].data, (oversamp,oversamp)) assert np.allclose(psf_detdist, psf_detdist_v2), "PSFs calculated should be equivalent for IPC convolution and binning in either order" + + +def test_ipc_basic_effect_on_psf_fwhm(): + """A basic test that the IPC model has the expected effect: making PSFs slightly broader. + + Tests that (a) there is no change to the first two extensions, which are 'pure optical PSF' + (b) that the FWHM increases for the other two extensions, which are distortion+detector effects + """ + nrc = webbpsf_core.NIRCam() + psf_withipc = nrc.calc_psf(nlambda=1, fov_pixels=101) + nrc.options['add_ipc'] = False + psf_noipc = nrc.calc_psf(nlambda=1, fov_pixels=101) + + for extname in ['OVERSAMP', 'DET_SAMP']: + fwhm_ipc = poppy.measure_fwhm(psf_withipc, ext=extname) + fwhm_noipc = poppy.measure_fwhm(psf_noipc, ext=extname) + assert fwhm_ipc==fwhm_noipc, f'Adding IPC should not have any effect on the {extname} data.' + print(f'test ok for {extname}') + + for extname in ['OVERDIST', 'DET_DIST']: + fwhm_ipc = poppy.measure_fwhm(psf_withipc, ext=extname) + fwhm_noipc = poppy.measure_fwhm(psf_noipc, ext=extname) + assert fwhm_ipc>fwhm_noipc, f'Adding IPC should not blur and increase the FWHM in the {extname} data.' + print(f'test ok for {extname}') From 2c65265e779feaefb7a27381d21c3702b3976a27 Mon Sep 17 00:00:00 2001 From: Marshall Perrin Date: Mon, 1 Apr 2024 12:11:47 -0400 Subject: [PATCH 12/12] fix test failure --- webbpsf/gridded_library.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/webbpsf/gridded_library.py b/webbpsf/gridded_library.py index 1fcb6a0e..bb259fc2 100644 --- a/webbpsf/gridded_library.py +++ b/webbpsf/gridded_library.py @@ -291,10 +291,11 @@ def create_grid(self): if self.verbose is True: print(" Position {}/{}: {} pixels".format(i+1, len(self.location_list), loc)) - add_ipc_gridded = self.webb.options.get('add_ipc', False) # add variable to keep track of the user's IPC input + add_ipc_gridded = False # add variable to keep track of if this function locally changed the user's IPC input # Deactivate IPC corrections, if any, before calc_psf as we are applying them later if self.webb.options.get('add_ipc', True): self.webb.options['add_ipc'] = False + add_ipc_gridded = True # we should reset the IPC flag below, after computing the PSF # Create PSF psf = self.webb.calc_psf(**self._kwargs) @@ -309,7 +310,7 @@ def create_grid(self): # Convolve PSF with a model for interpixel capacitance if self.add_distortion and add_ipc_gridded: webbpsf.detectors.apply_detector_ipc(psf, extname=ext) - self.webb.options['add_ipc'] = add_ipc_gridded# restore the user's value for the IPC option + self.webb.options['add_ipc'] = True # restore the user's value for the IPC option # Add PSF to 5D array psf_arr[i, :, :] = psf[ext].data