diff --git a/scopesim/effects/apertures.py b/scopesim/effects/apertures.py index 8dffa555..fd991fd6 100644 --- a/scopesim/effects/apertures.py +++ b/scopesim/effects/apertures.py @@ -78,6 +78,8 @@ class ApertureMask(Effect): """ + required_keys = {"filename", "table", "array_dict"} + def __init__(self, **kwargs): if not np.any([key in kwargs for key in ["filename", "table", "array_dict"]]): @@ -108,7 +110,6 @@ def __init__(self, **kwargs): self._mask = None self.mask_sum = None - self.required_keys = ["filename", "table", "array_dict"] check_keys(kwargs, self.required_keys, "warning", all_any="any") def apply_to(self, obj, **kwargs): @@ -200,13 +201,15 @@ def plot(self, axes=None): class RectangularApertureMask(ApertureMask): + required_keys = {"x", "y", "width", "height"} + def __init__(self, **kwargs): super().__init__(**kwargs) params = {"x_unit": "arcsec", "y_unit": "arcsec"} self.meta.update(params) self.meta.update(kwargs) - check_keys(self.meta, ["x", "y", "width", "height"]) + check_keys(self.meta, self.required_keys) self.table = self.get_table(**kwargs) @@ -284,8 +287,9 @@ def __init__(self, **kwargs): self.meta.update(kwargs) if self.table is not None: - required_keys = ["id", "left", "right", "top", "bottom", "angle", - "conserve_image", "shape"] + # Why not always? + required_keys = {"id", "left", "right", "top", "bottom", "angle", + "conserve_image", "shape"} check_keys(self.table.colnames, required_keys) def apply_to(self, obj, **kwargs): diff --git a/scopesim/effects/effects.py b/scopesim/effects/effects.py index b003f1b6..f1dc299b 100644 --- a/scopesim/effects/effects.py +++ b/scopesim/effects/effects.py @@ -34,6 +34,8 @@ class Effect(DataContainer): """ + required_keys = set() + def __init__(self, filename=None, **kwargs): super().__init__(filename=filename, **kwargs) self.meta["z_order"] = [] diff --git a/scopesim/effects/electronic.py b/scopesim/effects/electronic.py index 8504ca79..e469f4c0 100644 --- a/scopesim/effects/electronic.py +++ b/scopesim/effects/electronic.py @@ -84,14 +84,15 @@ class DetectorModePropertiesSetter(Effect): """ + required_keys = {"mode_properties"} + def __init__(self, **kwargs): super().__init__(**kwargs) params = {"z_order": [299, 900]} self.meta.update(params) self.meta.update(kwargs) - required_keys = ["mode_properties"] - check_keys(self.meta, required_keys, action="error") + check_keys(self.meta, self.required_keys, action="error") self.mode_properties = kwargs["mode_properties"] @@ -176,6 +177,8 @@ class AutoExposure(Effect): """ + required_keys = {"fill_frac", "full_well", "mindit"} + def __init__(self, **kwargs): """ The effect is the first detector effect, hence essentially operates @@ -189,8 +192,7 @@ def __init__(self, **kwargs): from scopesim import UserCommands self.cmds = UserCommands() - required_keys = ["fill_frac", "full_well", "mindit"] - check_keys(self.meta, required_keys, action="error") + check_keys(self.meta, self.required_keys, action="error") def apply_to(self, obj, **kwargs): if isinstance(obj, (ImagePlaneBase, DetectorBase)): @@ -238,14 +240,15 @@ def apply_to(self, obj, **kwargs): class SummedExposure(Effect): """Simulates a summed stack of ``ndit`` exposures.""" + required_keys = {"dit", "ndit"} + def __init__(self, **kwargs): super().__init__(**kwargs) params = {"z_order": [860]} self.meta.update(params) self.meta.update(kwargs) - required_keys = ["dit", "ndit"] - check_keys(self.meta, required_keys, action="error") + check_keys(self.meta, self.required_keys, action="error") def apply_to(self, obj, **kwargs): if isinstance(obj, DetectorBase): @@ -260,14 +263,15 @@ def apply_to(self, obj, **kwargs): class Bias(Effect): """Adds a constant bias level to readout.""" + required_keys = {"bias"} + def __init__(self, **kwargs): super().__init__(**kwargs) params = {"z_order": [855]} self.meta.update(params) self.meta.update(kwargs) - required_keys = ["bias"] - check_keys(self.meta, required_keys, action="error") + check_keys(self.meta, self.required_keys, action="error") def apply_to(self, obj, **kwargs): if isinstance(obj, DetectorBase): @@ -277,20 +281,23 @@ def apply_to(self, obj, **kwargs): return obj class PoorMansHxRGReadoutNoise(Effect): + required_keys = {"noise_std", "n_channels", "ndit"} + def __init__(self, **kwargs): super().__init__(**kwargs) - params = {"z_order": [811], - "pedestal_fraction": 0.3, - "read_fraction": 0.4, - "line_fraction": 0.25, - "channel_fraction": 0.05, - "random_seed": "!SIM.random.seed", - "report_plot_include": False, - "report_table_include": False} + params = { + "z_order": [811], + "pedestal_fraction": 0.3, + "read_fraction": 0.4, + "line_fraction": 0.25, + "channel_fraction": 0.05, + "random_seed": "!SIM.random.seed", + "report_plot_include": False, + "report_table_include": False, + } self.meta.update(params) self.meta.update(kwargs) - self.required_keys = ["noise_std", "n_channels", "ndit"] check_keys(self.meta, self.required_keys, action="error") def apply_to(self, det, **kwargs): @@ -332,13 +339,14 @@ def plot_hist(self, det, **kwargs): class BasicReadoutNoise(Effect): """Readout noise computed as: ron * sqrt(NDIT).""" + required_keys = {"noise_std", "ndit"} + def __init__(self, **kwargs): super().__init__(**kwargs) self.meta["z_order"] = [811] self.meta["random_seed"] = "!SIM.random.seed" self.meta.update(kwargs) - self.required_keys = ["noise_std", "ndit"] check_keys(self.meta, self.required_keys, action="error") def apply_to(self, det, **kwargs): @@ -417,12 +425,14 @@ class DarkCurrent(Effect): """ required: dit, ndit, value """ + + required_keys = {"value", "dit", "ndit"} + def __init__(self, **kwargs): super().__init__(**kwargs) self.meta["z_order"] = [830] - required_keys = ["value", "dit", "ndit"] - check_keys(self.meta, required_keys, action="error") + check_keys(self.meta, self.required_keys, action="error") def apply_to(self, obj, **kwargs): if isinstance(obj, DetectorBase): @@ -487,6 +497,8 @@ class LinearityCurve(Effect): """ + required_keys = {"ndit"} + def __init__(self, **kwargs): super().__init__(**kwargs) params = { @@ -497,7 +509,6 @@ def __init__(self, **kwargs): self.meta.update(params) self.meta.update(kwargs) - self.required_keys = ["ndit"] check_keys(self.meta, self.required_keys, action="error") def apply_to(self, obj, **kwargs): @@ -562,11 +573,12 @@ def plot(self, implane, **kwargs): class BinnedImage(Effect): + required_keys = {"bin_size"} + def __init__(self, **kwargs): super().__init__(**kwargs) self.meta["z_order"] = [870] - self.required_keys = ["bin_size"] check_keys(self.meta, self.required_keys, action="error") def apply_to(self, det, **kwargs): @@ -580,11 +592,12 @@ def apply_to(self, det, **kwargs): return det class UnequalBinnedImage(Effect): + required_keys = {"binx","biny"} + def __init__(self, **kwargs): super().__init__(**kwargs) self.meta["z_order"] = [870] - self.required_keys = ["binx","biny"] check_keys(self.meta, self.required_keys, action="error") def apply_to(self, det, **kwargs): diff --git a/scopesim/effects/obs_strategies.py b/scopesim/effects/obs_strategies.py index 8081bc4e..b34bd37e 100644 --- a/scopesim/effects/obs_strategies.py +++ b/scopesim/effects/obs_strategies.py @@ -50,8 +50,10 @@ class ChopNodCombiner(Effect): """ + required_keys = {"chop_offsets", "pixel_scale"} + def __init__(self, **kwargs): - check_keys(kwargs, ["chop_offsets", "pixel_scale"]) + check_keys(kwargs, self.required_keys) super().__init__(**kwargs) params = { diff --git a/scopesim/effects/psfs.py b/scopesim/effects/psfs.py index d6c2d0c7..1926563d 100644 --- a/scopesim/effects/psfs.py +++ b/scopesim/effects/psfs.py @@ -166,13 +166,14 @@ def __init__(self, **kwargs): class Vibration(AnalyticalPSF): """Creates a wavelength independent kernel image.""" + required_keys = {"fwhm", "pixel_scale"} + def __init__(self, **kwargs): super().__init__(**kwargs) self.meta["z_order"] = [244, 744] self.meta["width_n_fwhms"] = 4 self.convolution_classes = ImagePlaneBase - self.required_keys = ["fwhm", "pixel_scale"] check_keys(self.meta, self.required_keys, action="error") self.kernel = None @@ -197,6 +198,8 @@ class NonCommonPathAberration(AnalyticalPSF): Accepted: kernel_width, strehl_drift """ + required_keys = {"pixel_scale"} + def __init__(self, **kwargs): super().__init__(**kwargs) self.meta["z_order"] = [241, 641] @@ -210,7 +213,6 @@ def __init__(self, **kwargs): self.valid_waverange = [0.1 * u.um, 0.2 * u.um] self.convolution_classes = FieldOfViewBase - self.required_keys = ["pixel_scale"] check_keys(self.meta, self.required_keys, action="error") def fov_grid(self, which="waveset", **kwargs): @@ -435,6 +437,8 @@ class AnisocadoConstPSF(SemiAnalyticalPSF): """ + required_keys = {"filename", "strehl", "wavelength"} + def __init__(self, **kwargs): super().__init__(**kwargs) params = { @@ -446,7 +450,6 @@ def __init__(self, **kwargs): self.meta.update(params) self.meta.update(kwargs) - self.required_keys = ["filename", "strehl", "wavelength"] check_keys(self.meta, self.required_keys, action="error") self.nmRms # check to see if it throws an error @@ -593,11 +596,13 @@ class FieldConstantPSF(DiscretePSF): For spectroscopy, a wavelength-dependent PSF cube is built, where for each wavelength the reference PSF is scaled proportional to wavelength. """ + + required_keys = {"filename"} + def __init__(self, **kwargs): # sub_pixel_flag and flux_accuracy are taken care of in PSF base class super().__init__(**kwargs) - self.required_keys = ["filename"] check_keys(self.meta, self.required_keys, action="error") self.meta["z_order"] = [262, 662] @@ -723,11 +728,12 @@ class FieldVaryingPSF(DiscretePSF): """ + required_keys = {"filename"} + def __init__(self, **kwargs): # sub_pixel_flag and flux_accuracy are taken care of in PSF base class super().__init__(**kwargs) - self.required_keys = ["filename"] check_keys(self.meta, self.required_keys, action="error") self.meta["z_order"] = [261, 661] diff --git a/scopesim/effects/rotation.py b/scopesim/effects/rotation.py index f419a335..11069a25 100644 --- a/scopesim/effects/rotation.py +++ b/scopesim/effects/rotation.py @@ -23,14 +23,15 @@ class Rotate90CCD(Effect): """ + required_keys = {"rotations"} + def __init__(self, **kwargs): super().__init__(**kwargs) params = {"z_order": [809]} self.meta.update(params) self.meta.update(kwargs) - required_keys = ["rotations"] - utils.check_keys(self.meta, required_keys, action="error") + utils.check_keys(self.meta, self.required_keys, action="error") def apply_to(self, obj, **kwargs): """See parent docstring.""" diff --git a/scopesim/effects/shifts.py b/scopesim/effects/shifts.py index 0bd62fb8..50750329 100644 --- a/scopesim/effects/shifts.py +++ b/scopesim/effects/shifts.py @@ -102,6 +102,17 @@ class AtmosphericDispersion(Shift3D): """ + required_keys = { + "airmass", + "temperature", + "humidity", + "pressure", + "latitude", + "altitude", + "pupil_angle", + "pixel_scale", + } + def __init__(self, **kwargs): super().__init__(**kwargs) params = { @@ -115,9 +126,7 @@ def __init__(self, **kwargs): self.meta.update(params) self.meta.update(kwargs) - required_keys = ["airmass", "temperature", "humidity", "pressure", - "latitude", "altitude", "pupil_angle", "pixel_scale"] - check_keys(self.meta, required_keys, action="error") + check_keys(self.meta, self.required_keys, action="error") def get_table(self, **kwargs): """ @@ -172,6 +181,18 @@ class AtmosphericDispersionCorrection(Shift3D): kwargs """ + required_keys = { + "airmass", + "temperature", + "humidity", + "pressure", + "latitude", + "altitude", + "pupil_angle", + "pixel_scale", + "wave_mid", + } + def __init__(self, **kwargs): super().__init__(**kwargs) self.meta["z_order"] = [632] @@ -181,10 +202,7 @@ def __init__(self, **kwargs): self.meta["efficiency"] = 1 self.apply_to_classes = FieldOfViewBase - required_keys = ["airmass", "temperature", "humidity", "pressure", - "latitude", "altitude", "pupil_angle", "pixel_scale", - "wave_mid"] - check_keys(self.meta, required_keys, action="error") + check_keys(self.meta, self.required_keys, action="error") if self.table is None: self.table = self.get_table() diff --git a/scopesim/effects/ter_curves.py b/scopesim/effects/ter_curves.py index fd4f2c9e..25f9c603 100644 --- a/scopesim/effects/ter_curves.py +++ b/scopesim/effects/ter_curves.py @@ -502,9 +502,10 @@ class TopHatFilterCurve(FilterCurve): """ + required_keys = {"transmission", "blue_cutoff", "red_cutoff"} + def __init__(self, cmds=None, **kwargs): - required_keys = ["transmission", "blue_cutoff", "red_cutoff"] - check_keys(kwargs, required_keys, action="error") + check_keys(kwargs, self.required_keys, action="error") self.cmds = cmds wave_min = from_currsys("!SIM.spectral.wave_min", self.cmds) @@ -525,9 +526,10 @@ def __init__(self, cmds=None, **kwargs): class DownloadableFilterCurve(FilterCurve): + required_keys = {"filter_name", "filename_format"} + def __init__(self, **kwargs): - required_keys = ["filter_name", "filename_format"] - check_keys(kwargs, required_keys, action="error") + check_keys(kwargs, self.required_keys, action="error") filt_str = kwargs["filename_format"].format(kwargs["filter_name"]) tbl = download_svo_filter(filt_str, return_style="table") super().__init__(table=tbl, **kwargs) @@ -556,9 +558,10 @@ class SpanishVOFilterCurve(FilterCurve): """ + required_keys = {"observatory", "instrument", "filter_name"} + def __init__(self, **kwargs): - required_keys = ["observatory", "instrument", "filter_name"] - check_keys(kwargs, required_keys, action="error") + check_keys(kwargs, self.required_keys, action="error") filt_str = "{}/{}.{}".format(kwargs["observatory"], kwargs["instrument"], kwargs["filter_name"]) @@ -572,7 +575,6 @@ def __init__(self, **kwargs): class FilterWheelBase(Effect): """Base class for Filter Wheels.""" - required_keys = set() _current_str = "current_filter" def __init__(self, **kwargs): diff --git a/scopesim/optics/fov_manager_utils.py b/scopesim/optics/fov_manager_utils.py index 95a40f8b..7507f473 100644 --- a/scopesim/optics/fov_manager_utils.py +++ b/scopesim/optics/fov_manager_utils.py @@ -38,8 +38,13 @@ def get_3d_shifts(effects, **kwargs): - x_shift, y_shift: [deg] """ - required_keys = ["wave_min", "wave_mid", "wave_max", - "sub_pixel_fraction", "pixel_scale"] + required_keys = { + "wave_min", + "wave_mid", + "wave_max", + "sub_pixel_fraction", + "pixel_scale", + } check_keys(kwargs, required_keys, action="warning") effects = get_all_effects(effects, efs.Shift3D) @@ -100,7 +105,7 @@ def get_imaging_waveset(effects_list, **kwargs): [um] list of wavelengths """ - required_keys = ["wave_min", "wave_max"] + required_keys = {"wave_min", "wave_max"} check_keys(kwargs, required_keys, action="error") # get the filter wavelengths first to set (wave_min, wave_max) @@ -163,8 +168,12 @@ def get_imaging_headers(effects, **kwargs): # if larger than max_chunk_size, split into smaller headers # add image plane WCS information for a direct projection - required_keys = ["pixel_scale", "plate_scale", - "chunk_size", "max_segment_size"] + required_keys = { + "pixel_scale", + "plate_scale", + "chunk_size", + "max_segment_size", + } check_keys(kwargs, required_keys, action="error") plate_scale = kwargs["plate_scale"] # ["/mm] @@ -284,8 +293,12 @@ def get_imaging_fovs(headers, waveset, shifts, **kwargs): def get_spectroscopy_headers(effects, **kwargs): """Return generator of Header objects.""" - required_keys = ["pixel_scale", "plate_scale", - "wave_min", "wave_max"] + required_keys = { + "pixel_scale", + "plate_scale", + "wave_min", + "wave_max", + } check_keys(kwargs, required_keys, action="error") surface_list_effects = get_all_effects(effects, (efs.SurfaceList, diff --git a/scopesim/utils.py b/scopesim/utils.py index 2cf14b01..381522d7 100644 --- a/scopesim/utils.py +++ b/scopesim/utils.py @@ -5,9 +5,9 @@ import sys import logging from logging.config import dictConfig -from collections.abc import Iterable, Generator +from collections.abc import Iterable, Generator, Set, Mapping from copy import deepcopy -from typing import TextIO +from typing import TextIO, Union from io import StringIO from importlib import metadata import functools @@ -555,27 +555,62 @@ def from_rc_config(item): return from_currsys(item, rc.__config__) -def check_keys(input_dict, required_keys, action="error", all_any="all"): - """Check to see if all/any of the required keys are present in a dict.""" - if isinstance(input_dict, (list, tuple)): - input_dict = {key: None for key in input_dict} +def check_keys(input_dict: Union[Mapping, Iterable], + required_keys: Set, + action: str = "error", + all_any: str = "all") -> bool: + """ + Check to see if all/any of the required keys are present in a dict. + + .. versionchanged:: v0.8.0 + The `required_keys` parameter should now be a set. + + Parameters + ---------- + input_dict : Union[Mapping, Iterable] + The mapping to be checked. + required_keys : Set + Set containing the keys to look for. + action : {"error", "warn", "warning"}, optional + What to do in case the check does not pass. The default is "error". + all_any : {"all", "any"}, optional + Whether to check if "all" or "any" of the `required_keys` are present. + The default is "all". + + Raises + ------ + ValueError + Raised when an invalid parameter was passed or when `action` was set to + "error" (the default) and the `required_keys` were not found. + + Returns + ------- + keys_present : bool + ``True`` if check succeded, ``False`` otherwise. + + """ + # Checking for Set from collections.abc instead of builtin set to allow + # for any duck typing (e.g. dict keys view or whatever) + if not isinstance(required_keys, Set): + logger.warning("required_keys should implement the Set protocol, " + "found %s instead.", type(required_keys)) + required_keys = set(required_keys) if all_any == "all": - keys_present = all(key in input_dict for key in required_keys) + keys_present = required_keys.issubset(input_dict) elif all_any == "any": - keys_present = any(key in input_dict for key in required_keys) + keys_present = not required_keys.isdisjoint(input_dict) else: raise ValueError("all_any must be either 'all' or 'any'") if not keys_present: + missing = "', '".join(required_keys.difference(input_dict)) or "" if "error" in action: - raise ValueError("One or more of the following keys missing from " - f"input_dict: \n{required_keys} " - f"\n{input_dict.keys()}") + raise ValueError( + f"The keys '{missing}' are missing from input_dict.") if "warn" in action: logger.warning( - "One or more of the following keys missing from input_dict: " - "\n%s \n%s", required_keys, input_dict.keys()) + "The keys '%s' are missing from input_dict.", missing) return keys_present