From fb6577370e668bf71b9d5a127722dbd39519c95d Mon Sep 17 00:00:00 2001 From: Robel Geda Date: Wed, 24 Feb 2021 12:59:46 -0500 Subject: [PATCH 01/13] add new modes (sepctral) for Roman WFI --- webbpsf/roman.py | 83 +++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 72 insertions(+), 11 deletions(-) diff --git a/webbpsf/roman.py b/webbpsf/roman.py index c9a09958..ff69a8c2 100644 --- a/webbpsf/roman.py +++ b/webbpsf/roman.py @@ -302,7 +302,7 @@ def __init__(self): self._masked_pupil_path = None # List of filters that need the masked pupil - self._masked_filters = ['F184'] + self._masked_filters = ['F184', 'G150'] # Flag to en-/disable automatic selection of the appropriate pupil_mask self.auto_pupil = True @@ -473,15 +473,28 @@ def __init__(self): # Initialize the pupil controller self._pupil_controller = WFIPupilController() + # Initialize the aberrations for super().__init__ + self._aberrations_files = {} + self._is_custom_aberrations = False + self._current_aberrations_file = "" + super(WFI, self).__init__("WFI", pixelscale=pixelscale) self._pupil_controller.set_base_path(self._datapath) self.pupil_mask_list = self._pupil_controller.pupil_mask_list + # Define defualt aberration files for WFI modes + self._aberrations_files = { + 'imaging': os.path.join(self._datapath, 'wim_zernikes_cycle8.csv'), + 'prisim': os.path.join(self._datapath, 'wim_zernikes_cycle8_prism.csv'), + 'grism': os.path.join(self._datapath, 'wim_zernikes_cycle8_grism.csv'), + 'custom': None, + } + + # Load default detector from aberration file self._detector_npixels = 4096 - self._detectors = _load_wfi_detector_aberrations(os.path.join(self._datapath, 'wim_zernikes_cycle8.csv')) - assert len(self._detectors.keys()) > 0 + self._load_detector_aberrations(self._aberrations_files[self.mode]) self.detector = 'SCA01' self.opd_list = [ @@ -489,6 +502,11 @@ def __init__(self): ] self.pupilopd = self.opd_list[-1] + def _load_detector_aberrations(self, path): + self._detectors = _load_wfi_detector_aberrations(path) + self._current_aberrations_file = path + assert len(self._detectors.keys()) > 0 + def _validate_config(self, **kwargs): """Validates that the WFI is configured sensibly @@ -520,14 +538,6 @@ def pupil(self, value): def pupil_mask(self): return self._pupil_controller.pupil_mask - @RomanInstrument.filter.setter - def filter(self, value): - value = value.upper() # force to uppercase - if value not in self.filter_list: - raise ValueError("Instrument %s doesn't have a filter called %s." % (self.name, value)) - self._filter = value - self._pupil_controller.validate_pupil(self.filter) - @pupil_mask.setter def pupil_mask(self, name): """ @@ -559,6 +569,57 @@ def _masked_pupil_path(self): return self._pupil_controller._masked_pupil_path + def _get_filter_mode(self, wfi_filter): + if wfi_filter == 'G150': + return 'grism' + elif wfi_filter == 'P127': + return 'prisim' + elif wfi_filter in self.filter_list: + return 'imaging' + + @property + def mode(self): + return self._get_filter_mode(self.filter) + + def override_aberrations(self, aberrations_path): + """Override and lock detector aberrations""" + self._load_detector_aberrations(aberrations_path) + self._aberrations_files['custom'] = aberrations_path + self._is_custom_aberrations = True + + def reset_override_aberrations(self): + """Release detector aberrations override and load defaults""" + aberrations_path = self._aberrations_files[self.mode] + self._load_detector_aberrations(aberrations_path) + self._aberrations_files['custom'] = None + self._is_custom_aberrations = False + + # create properties with error checking + + @RomanInstrument.filter.setter + def filter(self, value): + value = value.upper() # force to uppercase + + if value not in self.filter_list: + raise ValueError("Instrument %s doesn't have a filter called %s." % (self.name, value)) + + self._filter = value + + # Check if _aberrations_files has been initiated (not empty) and if aberrations are locked by user + if self._aberrations_files and not self._is_custom_aberrations: + + # Identify aberrations file for new mode + mode = self._get_filter_mode(self._filter) + aberrations_file = self._aberrations_files[mode] + + # If aberrations are not already loaded for the new mode, + # load or replace detectors using the new mode's aberrations file. + if not os.path.samefile(self._current_aberrations_file, aberrations_file): + self._load_detector_aberrations(aberrations_file) + + self._pupil_controller.validate_pupil(self._filter) + + class CGI(RomanInstrument): """ Roman Coronagraph Instrument From 9a8a80de7b5387393db85aa3142233d6c878d881 Mon Sep 17 00:00:00 2001 From: Robel Geda Date: Wed, 24 Feb 2021 13:12:05 -0500 Subject: [PATCH 02/13] remove comment --- webbpsf/roman.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/webbpsf/roman.py b/webbpsf/roman.py index ff69a8c2..93a07d1d 100644 --- a/webbpsf/roman.py +++ b/webbpsf/roman.py @@ -594,8 +594,6 @@ def reset_override_aberrations(self): self._aberrations_files['custom'] = None self._is_custom_aberrations = False - # create properties with error checking - @RomanInstrument.filter.setter def filter(self, value): value = value.upper() # force to uppercase From 38dac7f4e6fdbacfdc09bcefb9b593cc0622c694 Mon Sep 17 00:00:00 2001 From: Robel Geda Date: Wed, 24 Feb 2021 13:37:47 -0500 Subject: [PATCH 03/13] Doc strings and comments for WFI modes --- webbpsf/roman.py | 45 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/webbpsf/roman.py b/webbpsf/roman.py index 93a07d1d..a02876fd 100644 --- a/webbpsf/roman.py +++ b/webbpsf/roman.py @@ -20,6 +20,8 @@ _log = logging.getLogger('webbpsf') import pprint +GRISM_FILTER = 'G150' +PRISM_FILTER = 'P127' class WavelengthDependenceInterpolator(object): """WavelengthDependenceInterpolator can be configured with @@ -302,7 +304,7 @@ def __init__(self): self._masked_pupil_path = None # List of filters that need the masked pupil - self._masked_filters = ['F184', 'G150'] + self._masked_filters = ['F184', GRISM_FILTER] # Flag to en-/disable automatic selection of the appropriate pupil_mask self.auto_pupil = True @@ -544,7 +546,7 @@ def pupil_mask(self, name): Set the pupil mask Parameters - ------------ + ---------- name : string Name of setting. Settings: @@ -570,17 +572,45 @@ def _masked_pupil_path(self): def _get_filter_mode(self, wfi_filter): - if wfi_filter == 'G150': + """ + Given a filter name, return the WFI mode + + Parameters + ---------- + wfi_filter : string + Name of WFI filter + + Returns + ------- + mode : string + Returns 'imaging', 'grism' or 'prisim' depending on filter. + + Raises + ------ + ValueError + If the input filter is not found in the WFI filter list + """ + + wfi_filter = wfi_filter.upper() + if wfi_filter == GRISM_FILTER: return 'grism' - elif wfi_filter == 'P127': + elif wfi_filter == PRISM_FILTER: return 'prisim' elif wfi_filter in self.filter_list: return 'imaging' + else: + raise ValueError("Instrument %s doesn't have a filter called %s." % (self.name, wfi_filter)) @property def mode(self): + """Current WFI mode""" return self._get_filter_mode(self.filter) + @mode.setter + def mode(self, value): + """Mode is set by changing filters""" + raise AttributeError("WFI mode can not be directly specified; WFI mode is set by changing filters.") + def override_aberrations(self, aberrations_path): """Override and lock detector aberrations""" self._load_detector_aberrations(aberrations_path) @@ -596,6 +626,9 @@ def reset_override_aberrations(self): @RomanInstrument.filter.setter def filter(self, value): + + # Update Filter + # ------------- value = value.upper() # force to uppercase if value not in self.filter_list: @@ -603,6 +636,8 @@ def filter(self, value): self._filter = value + # Update Aberrations + # ------------------ # Check if _aberrations_files has been initiated (not empty) and if aberrations are locked by user if self._aberrations_files and not self._is_custom_aberrations: @@ -615,6 +650,8 @@ def filter(self, value): if not os.path.samefile(self._current_aberrations_file, aberrations_file): self._load_detector_aberrations(aberrations_file) + # Update Pupil + # ------------ self._pupil_controller.validate_pupil(self._filter) From e03ce3645a09054ef44864a4c67e3e3db68275b4 Mon Sep 17 00:00:00 2001 From: Robel Geda Date: Wed, 24 Feb 2021 14:57:43 -0500 Subject: [PATCH 04/13] fix filter name (P120) --- webbpsf/roman.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/webbpsf/roman.py b/webbpsf/roman.py index a02876fd..43578ec8 100644 --- a/webbpsf/roman.py +++ b/webbpsf/roman.py @@ -21,7 +21,7 @@ import pprint GRISM_FILTER = 'G150' -PRISM_FILTER = 'P127' +PRISM_FILTER = 'P120' class WavelengthDependenceInterpolator(object): """WavelengthDependenceInterpolator can be configured with @@ -570,7 +570,6 @@ def _unmasked_pupil_path(self): def _masked_pupil_path(self): return self._pupil_controller._masked_pupil_path - def _get_filter_mode(self, wfi_filter): """ Given a filter name, return the WFI mode @@ -609,7 +608,7 @@ def mode(self): @mode.setter def mode(self, value): """Mode is set by changing filters""" - raise AttributeError("WFI mode can not be directly specified; WFI mode is set by changing filters.") + raise AttributeError("WFI mode cannot be directly specified; WFI mode is set by changing filters.") def override_aberrations(self, aberrations_path): """Override and lock detector aberrations""" @@ -646,7 +645,7 @@ def filter(self, value): aberrations_file = self._aberrations_files[mode] # If aberrations are not already loaded for the new mode, - # load or replace detectors using the new mode's aberrations file. + # load and replace detectors using the new mode's aberrations file. if not os.path.samefile(self._current_aberrations_file, aberrations_file): self._load_detector_aberrations(aberrations_file) From 234df7582067786bab84b98809a50969ae6f3fb4 Mon Sep 17 00:00:00 2001 From: Robel Geda Date: Fri, 26 Feb 2021 18:56:35 -0500 Subject: [PATCH 05/13] point travis to webbpsf-data-1.0.0.rc.tar --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index c58bcac9..696de2a9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -48,7 +48,7 @@ install: before_script: # Get WebbPSF data files (just a subset of the full 250 MB!) and set up environment variable - - wget https://stsci.box.com/shared/static/qcptcokkbx7fgi3c00w2732yezkxzb99.gz -O /tmp/minimal-webbpsf-data.tar.gz + - wget https://stsci.box.com/shared/static/ci3vkozwgyj82f1qle986k1hmeoggzzh.gz -O /tmp/minimal-webbpsf-data.tar.gz - tar -xzvf /tmp/minimal-webbpsf-data.tar.gz - export WEBBPSF_PATH="${TRAVIS_BUILD_DIR}/webbpsf-data" From 943a1a6de943b56c538aab30c84800004289c1b8 Mon Sep 17 00:00:00 2001 From: Robel Geda Date: Mon, 1 Mar 2021 13:58:25 -0500 Subject: [PATCH 06/13] add roman prism and grism tests --- webbpsf/tests/test_roman.py | 46 +++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/webbpsf/tests/test_roman.py b/webbpsf/tests/test_roman.py index 47896cdf..8771bcd3 100644 --- a/webbpsf/tests/test_roman.py +++ b/webbpsf/tests/test_roman.py @@ -3,6 +3,9 @@ from webbpsf import roman, measure_fwhm from numpy import allclose + +GRISM_FILTER = roman.GRISM_FILTER +PRISM_FILTER = roman.PRISM_FILTER MASKED_FLAG = "FULL_MASK" UNMASKED_FLAG = "RIM_MASK" AUTO_FLAG = "AUTO" @@ -190,8 +193,51 @@ def _test_filter_pupil(filter_name, expected_pupil): _test_filter_pupil('F062', wfi._unmasked_pupil_path) _test_filter_pupil('F158', wfi._unmasked_pupil_path) _test_filter_pupil('F146', wfi._unmasked_pupil_path) + _test_filter_pupil(PRISM_FILTER, wfi._unmasked_pupil_path) _test_filter_pupil('F184', wfi._masked_pupil_path) + _test_filter_pupil(GRISM_FILTER, wfi._masked_pupil_path) + +def test_swapping_modes(wfi=None): + + if wfi is None: + wfi = roman.WFI() + + tests = [ + # [filter, mode, pupil_file] + ['F062', 'imaging', wfi._unmasked_pupil_path], + ['F184', 'imaging', wfi._masked_pupil_path], + [PRISM_FILTER, 'prisim', wfi._unmasked_pupil_path], + [GRISM_FILTER, 'grism', wfi._masked_pupil_path], + ] + + for test_filter, test_mode, test_pupil in tests: + wfi.filter = test_filter + assert wfi.filter == test_filter + assert wfi.mode == test_mode + assert wfi._current_aberrations_file == wfi._aberrations_files[test_mode] + assert wfi.pupil == test_pupil + +def test_custom_aberrations(): + + wfi = roman.WFI() + + # Use grism aberrations_file for testing + test_aberrations_file = wfi._aberrations_files['grism'] + + # Test override + # ------------- + wfi.override_aberrations(test_aberrations_file) + + for filter in wfi.filter_list: + wfi.filter = filter + assert wfi._current_aberrations_file == test_aberrations_file, "Filter change caused override to fail" + + # Test Release Override + # --------------------- + wfi.reset_override_aberrations() + assert wfi._aberrations_files['custom'] is None, "Custom aberrations file not deleted on override release." + test_swapping_modes(wfi) def test_WFI_limits_interpolation_range(): wfi = roman.WFI() From 0cf595d93b51775df57e0c384b1f21d2f98b99f0 Mon Sep 17 00:00:00 2001 From: Robel Geda Date: Tue, 9 Mar 2021 12:11:49 -0500 Subject: [PATCH 07/13] spelling fix prisim -> prism --- webbpsf/roman.py | 6 +++--- webbpsf/tests/test_roman.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/webbpsf/roman.py b/webbpsf/roman.py index 43578ec8..e4327bf7 100644 --- a/webbpsf/roman.py +++ b/webbpsf/roman.py @@ -489,7 +489,7 @@ def __init__(self): # Define defualt aberration files for WFI modes self._aberrations_files = { 'imaging': os.path.join(self._datapath, 'wim_zernikes_cycle8.csv'), - 'prisim': os.path.join(self._datapath, 'wim_zernikes_cycle8_prism.csv'), + 'prism': os.path.join(self._datapath, 'wim_zernikes_cycle8_prism.csv'), 'grism': os.path.join(self._datapath, 'wim_zernikes_cycle8_grism.csv'), 'custom': None, } @@ -582,7 +582,7 @@ def _get_filter_mode(self, wfi_filter): Returns ------- mode : string - Returns 'imaging', 'grism' or 'prisim' depending on filter. + Returns 'imaging', 'grism' or 'prism' depending on filter. Raises ------ @@ -594,7 +594,7 @@ def _get_filter_mode(self, wfi_filter): if wfi_filter == GRISM_FILTER: return 'grism' elif wfi_filter == PRISM_FILTER: - return 'prisim' + return 'prism' elif wfi_filter in self.filter_list: return 'imaging' else: diff --git a/webbpsf/tests/test_roman.py b/webbpsf/tests/test_roman.py index 8771bcd3..0267fd09 100644 --- a/webbpsf/tests/test_roman.py +++ b/webbpsf/tests/test_roman.py @@ -207,7 +207,7 @@ def test_swapping_modes(wfi=None): # [filter, mode, pupil_file] ['F062', 'imaging', wfi._unmasked_pupil_path], ['F184', 'imaging', wfi._masked_pupil_path], - [PRISM_FILTER, 'prisim', wfi._unmasked_pupil_path], + [PRISM_FILTER, 'prism', wfi._unmasked_pupil_path], [GRISM_FILTER, 'grism', wfi._masked_pupil_path], ] From 02e45c7b2031e77cbd6b31c5a86a8673ceadfdaa Mon Sep 17 00:00:00 2001 From: Robel Geda Date: Tue, 9 Mar 2021 15:18:02 -0500 Subject: [PATCH 08/13] add roman doc-strings --- webbpsf/roman.py | 56 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 49 insertions(+), 7 deletions(-) diff --git a/webbpsf/roman.py b/webbpsf/roman.py index e4327bf7..82f3a07b 100644 --- a/webbpsf/roman.py +++ b/webbpsf/roman.py @@ -461,12 +461,6 @@ class WFI(RomanInstrument): def __init__(self): """ Initiate WFI - - Parameters - ----------- - set_pupil_mask_on : bool or None - Set to True or False to force using or not using the cold pupil mask, - or to None for the automatic behavior. """ # pixel scale is from Roman-AFTA SDT report final version (p. 91) # https://roman.ipac.caltech.edu/sims/Param_db.html @@ -505,6 +499,18 @@ def __init__(self): self.pupilopd = self.opd_list[-1] def _load_detector_aberrations(self, path): + """ + Helper function that, given a path to a file containing detector aberrations, loads the Zernike values and + populates the class' dictator list with `FieldDependentAberration` detectors. This function achieves this by + calling the `webbpsf.roman._load_wfi_detector_aberrations` function. + + Users should use the `override_aberrations` function to override current aberrations. + + Parameters + ---------- + path : string + Path to file containing detector aberrations + """ self._detectors = _load_wfi_detector_aberrations(path) self._current_aberrations_file = path assert len(self._detectors.keys()) > 0 @@ -611,7 +617,43 @@ def mode(self, value): raise AttributeError("WFI mode cannot be directly specified; WFI mode is set by changing filters.") def override_aberrations(self, aberrations_path): - """Override and lock detector aberrations""" + """ + This function loads user provided aberrations from a file and locks this instrument + to only use the provided aberrations (even if the filter or mode change). + To release the lock and load the default aberrations, use the `reset_override_aberrations` function. + To load new user provided aberrations, simply call this function with the new path. + + To load custom aberrations, please provide a csv file containing the detector names, + positions and Zernike values. The file should contain the following column names + (comments in parentheses should not be included): + - sca (Detector name) + - wavelength (µm) + - field_point (filed point number/id for SCA and wavelength, starts with 1) + - local_x (mm, local detector coords) + - local_y (mm, local detector coords) + - global_x (mm, global instrument coords) + - global_y (mm, global instrument coords) + - axis_local_angle_x (XAN) + - axis_local_angle_y (YAN) + - wfe_rms_waves (nm) + - wfe_pv_waves (waves) + - Z1 (Zernike phase NOLL coefficients) + - Z2 (Zernike phase NOLL coefficients) + - Z3 (Zernike phase NOLL coefficients) + - Z4 (Zernike phase NOLL coefficients) + . + . + . + + Please refer to the default aberrations files for examples. If you have the WebbPSF data installed and defined, + you can get the path to that file by running the following: + >>> from webbpsf import roman + >>> wfi = roman.WFI() + >>> print(wfi._aberrations_files["imaging"]) + + Warning: You should not edit the default files! + + """ self._load_detector_aberrations(aberrations_path) self._aberrations_files['custom'] = aberrations_path self._is_custom_aberrations = True From 90d82e5d1d1df7f0a3e2117f204220bf48718d85 Mon Sep 17 00:00:00 2001 From: Robel Geda Date: Tue, 9 Mar 2021 15:46:48 -0500 Subject: [PATCH 09/13] fix roman doc-strings --- webbpsf/roman.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/webbpsf/roman.py b/webbpsf/roman.py index 82f3a07b..2b5db947 100644 --- a/webbpsf/roman.py +++ b/webbpsf/roman.py @@ -624,9 +624,9 @@ def override_aberrations(self, aberrations_path): To load new user provided aberrations, simply call this function with the new path. To load custom aberrations, please provide a csv file containing the detector names, - positions and Zernike values. The file should contain the following column names + field point positions and Zernike values. The file should contain the following column names/values (comments in parentheses should not be included): - - sca (Detector name) + - sca (Detector number) - wavelength (µm) - field_point (filed point number/id for SCA and wavelength, starts with 1) - local_x (mm, local detector coords) @@ -652,7 +652,6 @@ def override_aberrations(self, aberrations_path): >>> print(wfi._aberrations_files["imaging"]) Warning: You should not edit the default files! - """ self._load_detector_aberrations(aberrations_path) self._aberrations_files['custom'] = aberrations_path From f049e092c02d0f64ee7dca7299d387d86b161fe2 Mon Sep 17 00:00:00 2001 From: Robel Geda Date: Tue, 9 Mar 2021 15:50:19 -0500 Subject: [PATCH 10/13] make _load_detector_aberrations safe --- webbpsf/roman.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/webbpsf/roman.py b/webbpsf/roman.py index 2b5db947..1c8a2522 100644 --- a/webbpsf/roman.py +++ b/webbpsf/roman.py @@ -511,9 +511,11 @@ def _load_detector_aberrations(self, path): path : string Path to file containing detector aberrations """ - self._detectors = _load_wfi_detector_aberrations(path) + detectors = _load_wfi_detector_aberrations(path) + assert len(detectors.keys()) > 0 + + self._detectors = detectors self._current_aberrations_file = path - assert len(self._detectors.keys()) > 0 def _validate_config(self, **kwargs): """Validates that the WFI is configured sensibly From e12cd40829de7e46d79ae2f1c63758da92176e7c Mon Sep 17 00:00:00 2001 From: Robel Geda Date: Mon, 3 May 2021 13:23:47 -0400 Subject: [PATCH 11/13] field point nearest point approximation --- webbpsf/roman.py | 17 +++++++++++++++-- webbpsf/tests/test_roman.py | 15 +++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/webbpsf/roman.py b/webbpsf/roman.py index 1c8a2522..67a181a7 100644 --- a/webbpsf/roman.py +++ b/webbpsf/roman.py @@ -161,7 +161,8 @@ def get_aberration_terms(self, wavelength): "(inconsistent number of Zernike terms " \ "at each point?)" - field_position = tuple(np.clip(self.field_position, 4, 4092)) + field_position = tuple(self.field_position) + coefficients = griddata( np.asarray(field_points), np.asarray(aberration_terms), @@ -169,7 +170,19 @@ def get_aberration_terms(self, wavelength): method='linear' ) if np.any(np.isnan(coefficients)): - raise RuntimeError("Could not get aberrations for input field point") + coefficients = griddata( + np.asarray(field_points), + np.asarray(aberration_terms), + field_position, + method='nearest' + ) + + assert not np.any(np.isnan(coefficients)), "Could not compute aberration " \ + "at field point {}".format(field_position) + + _log.warn("Attempted to get aberrations at field point {} which is outside the range " + "of the reference data; approximating to nearest field point".format(field_position)) + if self._omit_piston_tip_tilt: _log.debug("Omitting piston/tip/tilt") coefficients[:3] = 0.0 # omit piston, tip, and tilt Zernikes diff --git a/webbpsf/tests/test_roman.py b/webbpsf/tests/test_roman.py index 0267fd09..53d66843 100644 --- a/webbpsf/tests/test_roman.py +++ b/webbpsf/tests/test_roman.py @@ -1,4 +1,5 @@ import os +import numpy as np import pytest from webbpsf import roman, measure_fwhm from numpy import allclose @@ -282,6 +283,20 @@ def test_WFI_limits_interpolation_range(): "Aberration outside wavelength range did not return closest value." ) + # Test border pixels that are outside of the ref data + # As of cycle 8 and 9, (4, 4) is the first pixel so we + # check if (0, 0) is approximated to (4, 4) via nearest point + # approximation: + + det.field_position = (0, 0) + coefficients_outlier = det.get_aberration_terms(1e-6) + + det.field_position = (4, 4) + coefficients_data = det.get_aberration_terms(1e-6) + + assert np.allclose(coefficients_outlier, coefficients_data), "nearest point extrapolation " \ + "failed for outlier field point" + def test_CGI_detector_position(): """ Test existence of the CGI detector position etc, and that you can't set it.""" cgi = roman.CGI() From bda66c4e7cfcc4cd3cf20205f7f2548ba5e275a1 Mon Sep 17 00:00:00 2001 From: Robel Geda Date: Tue, 8 Jun 2021 23:17:07 -0400 Subject: [PATCH 12/13] add RegularGridInterpolator to Roman --- webbpsf/roman.py | 39 +++++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/webbpsf/roman.py b/webbpsf/roman.py index 67a181a7..bcc21dad 100644 --- a/webbpsf/roman.py +++ b/webbpsf/roman.py @@ -11,12 +11,16 @@ import os.path import poppy import numpy as np -from . import webbpsf_core -from scipy.interpolate import griddata + +from scipy.interpolate import griddata, RegularGridInterpolator from astropy.io import fits import astropy.units as u import logging +from . import webbpsf_core +from .optics import _fix_zgrid_NaNs + + _log = logging.getLogger('webbpsf') import pprint @@ -169,13 +173,32 @@ def get_aberration_terms(self, wavelength): field_position, method='linear' ) + if np.any(np.isnan(coefficients)): - coefficients = griddata( - np.asarray(field_points), - np.asarray(aberration_terms), - field_position, - method='nearest' - ) + # Create fine mesh grid + dstep = 1. / 2. # 0.5 pixel steps + + xgrid = np.arange(0, self.pixel_width + dstep, dstep) + ygrid = np.arange(0, self.pixel_height + dstep, dstep) + X, Y = np.meshgrid(xgrid, ygrid) + + # Cubic interpolation of all points + # Will produce a number of NaN's that need to be extrapolated over + zgrid = griddata(np.asarray(field_points), + np.asarray(aberration_terms), + (X, Y), method='cubic') + + # Fix the NaN's within zgrid array + # Perform specified rotation for certain SIs + # Trim rows/cols + zgrid = _fix_zgrid_NaNs(xgrid, ygrid, zgrid, rot_ang=0) + + # Create final function for extrapolation + func = RegularGridInterpolator((ygrid, xgrid), zgrid, method='linear', + bounds_error=False, fill_value=None) + + # Extrapolate at requested field_position coordinates + coefficients = func(field_position).tolist() assert not np.any(np.isnan(coefficients)), "Could not compute aberration " \ "at field point {}".format(field_position) From ab699bf4c5a23c33766331628447bb051b0ca632 Mon Sep 17 00:00:00 2001 From: Robel Geda Date: Wed, 9 Jun 2021 22:33:44 -0400 Subject: [PATCH 13/13] return to basic nearst extrapolation --- webbpsf/roman.py | 62 +++++++++++++++++++++++------------------------- 1 file changed, 30 insertions(+), 32 deletions(-) diff --git a/webbpsf/roman.py b/webbpsf/roman.py index bcc21dad..b6e5a2de 100644 --- a/webbpsf/roman.py +++ b/webbpsf/roman.py @@ -164,48 +164,46 @@ def get_aberration_terms(self, wavelength): assert len(aberration_array.shape) == 2, "computed aberration array is not 2D " \ "(inconsistent number of Zernike terms " \ "at each point?)" - field_position = tuple(self.field_position) - coefficients = griddata( np.asarray(field_points), np.asarray(aberration_terms), field_position, method='linear' ) - if np.any(np.isnan(coefficients)): - # Create fine mesh grid - dstep = 1. / 2. # 0.5 pixel steps - - xgrid = np.arange(0, self.pixel_width + dstep, dstep) - ygrid = np.arange(0, self.pixel_height + dstep, dstep) - X, Y = np.meshgrid(xgrid, ygrid) - - # Cubic interpolation of all points - # Will produce a number of NaN's that need to be extrapolated over - zgrid = griddata(np.asarray(field_points), - np.asarray(aberration_terms), - (X, Y), method='cubic') - - # Fix the NaN's within zgrid array - # Perform specified rotation for certain SIs - # Trim rows/cols - zgrid = _fix_zgrid_NaNs(xgrid, ygrid, zgrid, rot_ang=0) - - # Create final function for extrapolation - func = RegularGridInterpolator((ygrid, xgrid), zgrid, method='linear', - bounds_error=False, fill_value=None) - - # Extrapolate at requested field_position coordinates - coefficients = func(field_position).tolist() - + # FIND TWO CLOSEST INPUT GRID POINTS: + dist = [] + corners = field_points[1:] # use only the corner points + for i, ip in enumerate(corners): + dist.append(np.sqrt(((ip[0] - field_position[0]) ** 2) + ((ip[1] - field_position[1]) ** 2))) + min_dist_indx = np.argsort(dist)[:2] # keep two closest points + # DEFINE LINE B/W TWO POINTS, FIND ORTHOGONAL LINE AT POINT OF INTEREST, + # AND FIND INTERSECTION OF THESE TWO LINES. + x1, y1 = corners[min_dist_indx[0]] + x2, y2 = corners[min_dist_indx[1]] + dx = x2 - x1 + dy = y2 - y1 + a = (dy * (field_position[1] - y1) + dx * (field_position[0] - x1)) / (dx * dx + dy * dy) + closest_interp_point = (x1 + a * dx, y1 + a * dy) + # INTERPOLATE ABERRATIONS TO CLOSEST INTERPOLATED POINT: + coefficients = griddata( + np.asarray(field_points), + np.asarray(aberration_terms), + closest_interp_point, + method='linear') + # IF CLOSEST INTERPOLATED POINT IS STILL OUTSIDE THE INPUT GRID, + # THEN USE NEAREST GRID POINT INSTEAD: + if np.any(np.isnan(coefficients)): + coefficients = aberration_terms[min_dist_indx[0] + 1] + _log.warn("Attempted to get aberrations at field point {} which is outside the range " + "of the reference data; approximating to nearest input grid point".format(field_position)) + else: + _log.warn("Attempted to get aberrations at field point {} which is outside the range " + "of the reference data; approximating to nearest interpolated point {}".format( + field_position, closest_interp_point)) assert not np.any(np.isnan(coefficients)), "Could not compute aberration " \ "at field point {}".format(field_position) - - _log.warn("Attempted to get aberrations at field point {} which is outside the range " - "of the reference data; approximating to nearest field point".format(field_position)) - if self._omit_piston_tip_tilt: _log.debug("Omitting piston/tip/tilt") coefficients[:3] = 0.0 # omit piston, tip, and tilt Zernikes