From eaac2b818d9ba4360a72d0c9072806d1f619c0c9 Mon Sep 17 00:00:00 2001 From: Stephan Finkensieper Date: Wed, 12 Jun 2024 13:29:03 +0000 Subject: [PATCH 01/13] Add generic calibration coefficient selector --- satpy/readers/utils.py | 90 +++++++++++++++++++++++ satpy/tests/reader_tests/test_utils.py | 98 ++++++++++++++++++++++++++ 2 files changed, 188 insertions(+) diff --git a/satpy/readers/utils.py b/satpy/readers/utils.py index c1bf7c7497..aba50c5e2d 100644 --- a/satpy/readers/utils.py +++ b/satpy/readers/utils.py @@ -474,3 +474,93 @@ def remove_earthsun_distance_correction(reflectance, utc_date=None): with xr.set_options(keep_attrs=True): reflectance = reflectance / reflectance.dtype.type(sun_earth_dist * sun_earth_dist) return reflectance + + +class CalibrationCoefficientSelector: + """Helper for choosing coefficients out of multiple options.""" + + def __init__(self, coefs, modes=None, default="nominal", fallback=None, refl_threshold=1): + """Initialize the coefficient selector. + + Args: + coefs (dict): One set of calibration coefficients for each calibration + mode, for example :: + + { + "nominal": { + "ch1": nominal_coefs_ch1, + "ch2": nominal_coefs_ch2 + }, + "gsics": { + "ch2": gsics_coefs_ch2 + } + } + + The actual coefficients can be of any type (reader-specific). + + modes (dict): Desired calibration modes per channel type :: + + { + "reflective": "nominal", + "emissive": "gsics" + } + + or per channel :: + + { + "VIS006": "nominal", + "IR_108": "gsics" + } + + default (str): Default coefficients to be used if no mode has been + specified. Default: "nominal". + fallback (str): Fallback coefficients if the desired coefficients + are not available for some channel. + refl_threshold: Central wavelengths below/above this threshold are + considered reflective/emissive. Default is 1um. + """ + self.coefs = coefs + self.modes = modes or {} + self.default = default + self.fallback = fallback + self.refl_threshold = refl_threshold + if self.default not in self.coefs: + raise KeyError("Need at least default coefficients") + if self.fallback and self.fallback not in self.coefs: + raise KeyError("No fallback coefficients") + + def get_coefs(self, dataset_id): + """Get calibration coefficients for the given dataset. + + Args: + dataset_id (DataID): Desired dataset + """ + mode = self._get_mode(dataset_id) + return self._get_coefs(dataset_id, mode) + + def _get_coefs(self, dataset_id, mode): + ds_name = dataset_id["name"] + try: + return self.coefs[mode][ds_name] + except KeyError: + if self.fallback: + return self.coefs[self.fallback][ds_name] + raise KeyError(f"No calibration coefficients for {ds_name}") + + def _get_mode(self, dataset_id): + try: + return self._get_mode_for_channel(dataset_id) + except KeyError: + return self._get_mode_for_channel_type(dataset_id) + + def _get_mode_for_channel(self, dataset_id): + return self.modes[dataset_id["name"]] + + def _get_mode_for_channel_type(self, dataset_id): + ch_type = self._get_channel_type(dataset_id) + return self.modes.get(ch_type, self.default) + + def _get_channel_type(self, dataset_id): + if dataset_id["wavelength"].central < self.refl_threshold: + return "reflective" + return "emissive" diff --git a/satpy/tests/reader_tests/test_utils.py b/satpy/tests/reader_tests/test_utils.py index ba43688b76..26d709477a 100644 --- a/satpy/tests/reader_tests/test_utils.py +++ b/satpy/tests/reader_tests/test_utils.py @@ -34,6 +34,8 @@ from satpy.readers import FSFile from satpy.readers import utils as hf +from satpy.readers.utils import CalibrationCoefficientSelector +from satpy.tests.utils import make_dataid class TestHelpers(unittest.TestCase): @@ -512,3 +514,99 @@ def test_generic_open_binary(tmp_path, data, filename, mode): read_binary_data = f.read() assert read_binary_data == dummy_data + + +CALIB_COEFS = { + "nominal": { + "ch1": {"slope": 0.1, "offset": 1}, + "ch2": {"slope": 0.2, "offset": 2} + }, + "mode1": { + "ch1": {"slope": 0.3, "offset": 3}, + }, + "mode2": { + "ch2": {"slope": 0.5, "offset": 5}, + } +} + + +class TestCalibrationCoefficientSelector: + """Test selection of calibration coefficients.""" + + @pytest.fixture(name="ch1") + def fixture_ch1(self): + """Make fake data ID.""" + return make_dataid(name="ch1", wavelength=(0.6, 0.7, 0.8)) + + @pytest.fixture(name="ch2") + def fixture_ch2(self): + """Make fake data ID.""" + return make_dataid(name="ch2", wavelength=(10, 11, 12)) + + @pytest.fixture(name="dataset_ids") + def fixture_dataset_ids(self, ch1, ch2): + """Make fake data IDs.""" + return [ch1, ch2] + + @pytest.mark.parametrize( + ("calib_modes", "expected"), + [ + ( + None, + CALIB_COEFS["nominal"] + ), + ( + {"reflective": "mode1"}, + { + "ch1": CALIB_COEFS["mode1"]["ch1"], + "ch2": CALIB_COEFS["nominal"]["ch2"] + } + ), + ( + {"reflective": "mode1", "emissive": "mode2"}, + { + "ch1": CALIB_COEFS["mode1"]["ch1"], + "ch2": CALIB_COEFS["mode2"]["ch2"] + } + ), + ( + {"ch1": "mode1"}, + { + "ch1": CALIB_COEFS["mode1"]["ch1"], + "ch2": CALIB_COEFS["nominal"]["ch2"] + } + ), + ] + ) + def test_get_coefs(self, dataset_ids, calib_modes, expected): + """Test getting calibration coefficients.""" + s = CalibrationCoefficientSelector(CALIB_COEFS, calib_modes) + coefs = { + dataset_id["name"]: s.get_coefs(dataset_id) + for dataset_id in dataset_ids + } + assert coefs == expected + + def test_missing_coefs(self, ch1): + """Test handling of missing coefficients.""" + calib_modes = {"reflective": "mode2"} + s = CalibrationCoefficientSelector(CALIB_COEFS, calib_modes) + with pytest.raises(KeyError, match="No calibration *"): + s.get_coefs(ch1) + + def test_fallback_to_nominal(self, ch1): + """Test falling back to nominal coefficients.""" + calib_modes = {"reflective": "mode2"} + s = CalibrationCoefficientSelector(CALIB_COEFS, calib_modes, fallback="nominal") + coefs = s.get_coefs(ch1) + assert coefs == {"slope": 0.1, "offset": 1} + + def test_no_default_coefs(self): + """Test initialization without default coefficients.""" + with pytest.raises(KeyError, match="Need at least *"): + CalibrationCoefficientSelector({}) + + def test_no_fallback(self): + """Test initialization without fallback coefficients.""" + with pytest.raises(KeyError, match="No fallback coefficients"): + CalibrationCoefficientSelector({"nominal": 123}, fallback="foo") From 209f17df0450a02101eaf6e4adf0bd54464249f8 Mon Sep 17 00:00:00 2001 From: Stephan Finkensieper Date: Wed, 12 Jun 2024 13:39:13 +0000 Subject: [PATCH 02/13] Simplify tests --- satpy/tests/reader_tests/test_utils.py | 62 +++++++++++--------------- 1 file changed, 27 insertions(+), 35 deletions(-) diff --git a/satpy/tests/reader_tests/test_utils.py b/satpy/tests/reader_tests/test_utils.py index 26d709477a..ede1e0af97 100644 --- a/satpy/tests/reader_tests/test_utils.py +++ b/satpy/tests/reader_tests/test_utils.py @@ -516,23 +516,25 @@ def test_generic_open_binary(tmp_path, data, filename, mode): assert read_binary_data == dummy_data -CALIB_COEFS = { - "nominal": { - "ch1": {"slope": 0.1, "offset": 1}, - "ch2": {"slope": 0.2, "offset": 2} - }, - "mode1": { - "ch1": {"slope": 0.3, "offset": 3}, - }, - "mode2": { - "ch2": {"slope": 0.5, "offset": 5}, - } -} - - class TestCalibrationCoefficientSelector: """Test selection of calibration coefficients.""" + @pytest.fixture(name="coefs") + def fixture_coefs(self): + """Get fake coefficients.""" + return { + "nominal": { + "ch1": "nominal_ch1", + "ch2": "nominal_ch2" + }, + "mode1": { + "ch1": "mode1_ch1", + }, + "mode2": { + "ch2": "mode2_ch2", + } + } + @pytest.fixture(name="ch1") def fixture_ch1(self): """Make fake data ID.""" @@ -553,53 +555,43 @@ def fixture_dataset_ids(self, ch1, ch2): [ ( None, - CALIB_COEFS["nominal"] + {"ch1": "nominal_ch1", "ch2": "nominal_ch2"} ), ( {"reflective": "mode1"}, - { - "ch1": CALIB_COEFS["mode1"]["ch1"], - "ch2": CALIB_COEFS["nominal"]["ch2"] - } + {"ch1": "mode1_ch1", "ch2": "nominal_ch2"} ), ( {"reflective": "mode1", "emissive": "mode2"}, - { - "ch1": CALIB_COEFS["mode1"]["ch1"], - "ch2": CALIB_COEFS["mode2"]["ch2"] - } + {"ch1": "mode1_ch1", "ch2": "mode2_ch2"} ), ( {"ch1": "mode1"}, - { - "ch1": CALIB_COEFS["mode1"]["ch1"], - "ch2": CALIB_COEFS["nominal"]["ch2"] - } + {"ch1": "mode1_ch1", "ch2": "nominal_ch2"} ), ] ) - def test_get_coefs(self, dataset_ids, calib_modes, expected): + def test_get_coefs(self, dataset_ids, coefs, calib_modes, expected): """Test getting calibration coefficients.""" - s = CalibrationCoefficientSelector(CALIB_COEFS, calib_modes) + s = CalibrationCoefficientSelector(coefs, calib_modes) coefs = { dataset_id["name"]: s.get_coefs(dataset_id) for dataset_id in dataset_ids } assert coefs == expected - def test_missing_coefs(self, ch1): + def test_missing_coefs(self, coefs, ch1): """Test handling of missing coefficients.""" calib_modes = {"reflective": "mode2"} - s = CalibrationCoefficientSelector(CALIB_COEFS, calib_modes) + s = CalibrationCoefficientSelector(coefs, calib_modes) with pytest.raises(KeyError, match="No calibration *"): s.get_coefs(ch1) - def test_fallback_to_nominal(self, ch1): + def test_fallback_to_nominal(self, coefs, ch1): """Test falling back to nominal coefficients.""" calib_modes = {"reflective": "mode2"} - s = CalibrationCoefficientSelector(CALIB_COEFS, calib_modes, fallback="nominal") - coefs = s.get_coefs(ch1) - assert coefs == {"slope": 0.1, "offset": 1} + s = CalibrationCoefficientSelector(coefs, calib_modes, fallback="nominal") + assert s.get_coefs(ch1) == "nominal_ch1" def test_no_default_coefs(self): """Test initialization without default coefficients.""" From c4153ba121e65562667c946bb8b303878f832ecd Mon Sep 17 00:00:00 2001 From: Stephan Finkensieper Date: Wed, 12 Jun 2024 13:59:13 +0000 Subject: [PATCH 03/13] Improve error message --- satpy/readers/utils.py | 2 +- satpy/tests/reader_tests/test_utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/satpy/readers/utils.py b/satpy/readers/utils.py index aba50c5e2d..f976cb5557 100644 --- a/satpy/readers/utils.py +++ b/satpy/readers/utils.py @@ -545,7 +545,7 @@ def _get_coefs(self, dataset_id, mode): except KeyError: if self.fallback: return self.coefs[self.fallback][ds_name] - raise KeyError(f"No calibration coefficients for {ds_name}") + raise KeyError(f"No {mode} calibration coefficients for {ds_name}") def _get_mode(self, dataset_id): try: diff --git a/satpy/tests/reader_tests/test_utils.py b/satpy/tests/reader_tests/test_utils.py index ede1e0af97..dd0d1bd9b8 100644 --- a/satpy/tests/reader_tests/test_utils.py +++ b/satpy/tests/reader_tests/test_utils.py @@ -584,7 +584,7 @@ def test_missing_coefs(self, coefs, ch1): """Test handling of missing coefficients.""" calib_modes = {"reflective": "mode2"} s = CalibrationCoefficientSelector(coefs, calib_modes) - with pytest.raises(KeyError, match="No calibration *"): + with pytest.raises(KeyError, match="No mode2 calibration *"): s.get_coefs(ch1) def test_fallback_to_nominal(self, coefs, ch1): From dca1a1846225d0c47e9ae067ff6b962a230ee225 Mon Sep 17 00:00:00 2001 From: Stephan Finkensieper Date: Wed, 12 Jun 2024 15:46:32 +0000 Subject: [PATCH 04/13] Use explicit channel names --- satpy/readers/utils.py | 114 ++++++++++++++----------- satpy/tests/reader_tests/test_utils.py | 26 +++--- 2 files changed, 75 insertions(+), 65 deletions(-) diff --git a/satpy/readers/utils.py b/satpy/readers/utils.py index f976cb5557..f4743137b7 100644 --- a/satpy/readers/utils.py +++ b/satpy/readers/utils.py @@ -477,53 +477,79 @@ def remove_earthsun_distance_correction(reflectance, utc_date=None): class CalibrationCoefficientSelector: - """Helper for choosing coefficients out of multiple options.""" + """Helper for choosing coefficients out of multiple options. + + Example: Three sets of coefficients are available (nominal, meirink, gsics). + Calibrate channel 1 with "meirink" and channels 2/3 with "gsics". + + 1. Setup + + .. code-block:: python + + from satpy.readers.utils import CalibrationCoefficientSelector + from satpy.tests.utils import make_dataid + + coefs = { + "nominal": { + "ch1": "nominal_ch1", + "ch2": "nominal_ch2", + "ch3": "nominal_ch3" + }, + "meirink": { + "ch1": "meirink_ch1", + }, + "gsics": { + "ch2": "gsics_ch2", + # ch3 coefficients are missing + } + } + modes = { + "meirink": ["ch1"], + "gsics": ["ch2", "ch3"] + } + + ch1 = make_dataid(name="ch1") + ch2 = make_dataid(name="ch2") + ch3 = make_dataid(name="ch3") + + 2. Query: + + .. code-block:: python + + >>> s = CalibrationCoefficientSelector(coefs, modes) + >>> s.get_coefs(ch1) + "meirink_ch1" + >>> s.get_coefs(ch2) + "gsics_ch2" + >>> s.get_coefs(ch3) + KeyError: 'No gsics calibration coefficients for ch3' + + 3. Fallback to nominal for ch3: + + .. code-block:: python + + >>> s = CalibrationCoefficientSelector(coefs, modes, fallback="nominal") + >>> s.get_coefs(ch3) + "nominal_ch3" + """ - def __init__(self, coefs, modes=None, default="nominal", fallback=None, refl_threshold=1): + def __init__(self, coefs, modes=None, default="nominal", fallback=None): """Initialize the coefficient selector. Args: - coefs (dict): One set of calibration coefficients for each calibration - mode, for example :: - - { - "nominal": { - "ch1": nominal_coefs_ch1, - "ch2": nominal_coefs_ch2 - }, - "gsics": { - "ch2": gsics_coefs_ch2 - } - } - - The actual coefficients can be of any type (reader-specific). - - modes (dict): Desired calibration modes per channel type :: - - { - "reflective": "nominal", - "emissive": "gsics" - } - - or per channel :: - - { - "VIS006": "nominal", - "IR_108": "gsics" - } - + coefs (dict): One set of calibration coefficients for each + calibration mode. The actual coefficients can be of any type + (reader-specific). + modes (dict): Desired calibration modes default (str): Default coefficients to be used if no mode has been specified. Default: "nominal". fallback (str): Fallback coefficients if the desired coefficients are not available for some channel. - refl_threshold: Central wavelengths below/above this threshold are - considered reflective/emissive. Default is 1um. """ self.coefs = coefs self.modes = modes or {} self.default = default self.fallback = fallback - self.refl_threshold = refl_threshold if self.default not in self.coefs: raise KeyError("Need at least default coefficients") if self.fallback and self.fallback not in self.coefs: @@ -548,19 +574,7 @@ def _get_coefs(self, dataset_id, mode): raise KeyError(f"No {mode} calibration coefficients for {ds_name}") def _get_mode(self, dataset_id): - try: - return self._get_mode_for_channel(dataset_id) - except KeyError: - return self._get_mode_for_channel_type(dataset_id) - - def _get_mode_for_channel(self, dataset_id): - return self.modes[dataset_id["name"]] - - def _get_mode_for_channel_type(self, dataset_id): - ch_type = self._get_channel_type(dataset_id) - return self.modes.get(ch_type, self.default) - - def _get_channel_type(self, dataset_id): - if dataset_id["wavelength"].central < self.refl_threshold: - return "reflective" - return "emissive" + for mode, channels in self.modes.items(): + if dataset_id["name"] in channels: + return mode + return self.default diff --git a/satpy/tests/reader_tests/test_utils.py b/satpy/tests/reader_tests/test_utils.py index dd0d1bd9b8..18ca3d0ae0 100644 --- a/satpy/tests/reader_tests/test_utils.py +++ b/satpy/tests/reader_tests/test_utils.py @@ -538,16 +538,12 @@ def fixture_coefs(self): @pytest.fixture(name="ch1") def fixture_ch1(self): """Make fake data ID.""" - return make_dataid(name="ch1", wavelength=(0.6, 0.7, 0.8)) - - @pytest.fixture(name="ch2") - def fixture_ch2(self): - """Make fake data ID.""" - return make_dataid(name="ch2", wavelength=(10, 11, 12)) + return make_dataid(name="ch1") @pytest.fixture(name="dataset_ids") - def fixture_dataset_ids(self, ch1, ch2): + def fixture_dataset_ids(self, ch1): """Make fake data IDs.""" + ch2 = make_dataid(name="ch2") return [ch1, ch2] @pytest.mark.parametrize( @@ -558,16 +554,16 @@ def fixture_dataset_ids(self, ch1, ch2): {"ch1": "nominal_ch1", "ch2": "nominal_ch2"} ), ( - {"reflective": "mode1"}, - {"ch1": "mode1_ch1", "ch2": "nominal_ch2"} + {"nominal": ["ch1", "ch2"]}, + {"ch1": "nominal_ch1", "ch2": "nominal_ch2"} ), ( - {"reflective": "mode1", "emissive": "mode2"}, - {"ch1": "mode1_ch1", "ch2": "mode2_ch2"} + {"mode1": ["ch1"]}, + {"ch1": "mode1_ch1", "ch2": "nominal_ch2"} ), ( - {"ch1": "mode1"}, - {"ch1": "mode1_ch1", "ch2": "nominal_ch2"} + {"mode1": ["ch1"], "mode2": ["ch2"]}, + {"ch1": "mode1_ch1", "ch2": "mode2_ch2"} ), ] ) @@ -582,14 +578,14 @@ def test_get_coefs(self, dataset_ids, coefs, calib_modes, expected): def test_missing_coefs(self, coefs, ch1): """Test handling of missing coefficients.""" - calib_modes = {"reflective": "mode2"} + calib_modes = {"mode2": ["ch1"]} s = CalibrationCoefficientSelector(coefs, calib_modes) with pytest.raises(KeyError, match="No mode2 calibration *"): s.get_coefs(ch1) def test_fallback_to_nominal(self, coefs, ch1): """Test falling back to nominal coefficients.""" - calib_modes = {"reflective": "mode2"} + calib_modes = {"mode2": ["ch1"]} s = CalibrationCoefficientSelector(coefs, calib_modes, fallback="nominal") assert s.get_coefs(ch1) == "nominal_ch1" From 30aff090ea027313de49a8f3a86a4d02bda57bc3 Mon Sep 17 00:00:00 2001 From: Stephan Finkensieper Date: Thu, 13 Jun 2024 07:27:37 +0000 Subject: [PATCH 05/13] Use channel name instead of data ids --- satpy/readers/utils.py | 36 +++++++++++--------------- satpy/tests/reader_tests/test_utils.py | 26 +++++-------------- 2 files changed, 22 insertions(+), 40 deletions(-) diff --git a/satpy/readers/utils.py b/satpy/readers/utils.py index f4743137b7..d66e6537b0 100644 --- a/satpy/readers/utils.py +++ b/satpy/readers/utils.py @@ -487,7 +487,6 @@ class CalibrationCoefficientSelector: .. code-block:: python from satpy.readers.utils import CalibrationCoefficientSelector - from satpy.tests.utils import make_dataid coefs = { "nominal": { @@ -508,20 +507,16 @@ class CalibrationCoefficientSelector: "gsics": ["ch2", "ch3"] } - ch1 = make_dataid(name="ch1") - ch2 = make_dataid(name="ch2") - ch3 = make_dataid(name="ch3") - 2. Query: .. code-block:: python >>> s = CalibrationCoefficientSelector(coefs, modes) - >>> s.get_coefs(ch1) + >>> s.get_coefs("ch1") "meirink_ch1" - >>> s.get_coefs(ch2) + >>> s.get_coefs("ch2") "gsics_ch2" - >>> s.get_coefs(ch3) + >>> s.get_coefs("ch3") KeyError: 'No gsics calibration coefficients for ch3' 3. Fallback to nominal for ch3: @@ -529,7 +524,7 @@ class CalibrationCoefficientSelector: .. code-block:: python >>> s = CalibrationCoefficientSelector(coefs, modes, fallback="nominal") - >>> s.get_coefs(ch3) + >>> s.get_coefs("ch3") "nominal_ch3" """ @@ -555,26 +550,25 @@ def __init__(self, coefs, modes=None, default="nominal", fallback=None): if self.fallback and self.fallback not in self.coefs: raise KeyError("No fallback coefficients") - def get_coefs(self, dataset_id): - """Get calibration coefficients for the given dataset. + def get_coefs(self, channel): + """Get calibration coefficients for the given channel. Args: - dataset_id (DataID): Desired dataset + channel (str): Channel name """ - mode = self._get_mode(dataset_id) - return self._get_coefs(dataset_id, mode) + mode = self._get_mode(channel) + return self._get_coefs(channel, mode) - def _get_coefs(self, dataset_id, mode): - ds_name = dataset_id["name"] + def _get_coefs(self, channel, mode): try: - return self.coefs[mode][ds_name] + return self.coefs[mode][channel] except KeyError: if self.fallback: - return self.coefs[self.fallback][ds_name] - raise KeyError(f"No {mode} calibration coefficients for {ds_name}") + return self.coefs[self.fallback][channel] + raise KeyError(f"No {mode} calibration coefficients for {channel}") - def _get_mode(self, dataset_id): + def _get_mode(self, channel): for mode, channels in self.modes.items(): - if dataset_id["name"] in channels: + if channel in channels: return mode return self.default diff --git a/satpy/tests/reader_tests/test_utils.py b/satpy/tests/reader_tests/test_utils.py index 18ca3d0ae0..fd400bde08 100644 --- a/satpy/tests/reader_tests/test_utils.py +++ b/satpy/tests/reader_tests/test_utils.py @@ -35,7 +35,6 @@ from satpy.readers import FSFile from satpy.readers import utils as hf from satpy.readers.utils import CalibrationCoefficientSelector -from satpy.tests.utils import make_dataid class TestHelpers(unittest.TestCase): @@ -535,17 +534,6 @@ def fixture_coefs(self): } } - @pytest.fixture(name="ch1") - def fixture_ch1(self): - """Make fake data ID.""" - return make_dataid(name="ch1") - - @pytest.fixture(name="dataset_ids") - def fixture_dataset_ids(self, ch1): - """Make fake data IDs.""" - ch2 = make_dataid(name="ch2") - return [ch1, ch2] - @pytest.mark.parametrize( ("calib_modes", "expected"), [ @@ -567,27 +555,27 @@ def fixture_dataset_ids(self, ch1): ), ] ) - def test_get_coefs(self, dataset_ids, coefs, calib_modes, expected): + def test_get_coefs(self, coefs, calib_modes, expected): """Test getting calibration coefficients.""" s = CalibrationCoefficientSelector(coefs, calib_modes) coefs = { - dataset_id["name"]: s.get_coefs(dataset_id) - for dataset_id in dataset_ids + channel: s.get_coefs(channel) + for channel in ["ch1", "ch2"] } assert coefs == expected - def test_missing_coefs(self, coefs, ch1): + def test_missing_coefs(self, coefs): """Test handling of missing coefficients.""" calib_modes = {"mode2": ["ch1"]} s = CalibrationCoefficientSelector(coefs, calib_modes) with pytest.raises(KeyError, match="No mode2 calibration *"): - s.get_coefs(ch1) + s.get_coefs("ch1") - def test_fallback_to_nominal(self, coefs, ch1): + def test_fallback_to_nominal(self, coefs): """Test falling back to nominal coefficients.""" calib_modes = {"mode2": ["ch1"]} s = CalibrationCoefficientSelector(coefs, calib_modes, fallback="nominal") - assert s.get_coefs(ch1) == "nominal_ch1" + assert s.get_coefs("ch1") == "nominal_ch1" def test_no_default_coefs(self): """Test initialization without default coefficients.""" From e82e2b9b3e35c3f473603d86d602c3cee2efeed2 Mon Sep 17 00:00:00 2001 From: Stephan Finkensieper Date: Thu, 13 Jun 2024 07:46:14 +0000 Subject: [PATCH 06/13] Add option to specify a global calibration mode --- satpy/readers/utils.py | 16 +++++++++++++++- satpy/tests/reader_tests/test_utils.py | 4 ++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/satpy/readers/utils.py b/satpy/readers/utils.py index d66e6537b0..4a83c26f16 100644 --- a/satpy/readers/utils.py +++ b/satpy/readers/utils.py @@ -535,7 +535,9 @@ def __init__(self, coefs, modes=None, default="nominal", fallback=None): coefs (dict): One set of calibration coefficients for each calibration mode. The actual coefficients can be of any type (reader-specific). - modes (dict): Desired calibration modes + modes (str or dict): Desired calibration modes. Use a dictionary + `{mode: channels}` to specify multiple modes. Use a string to + specify one mode for all channels. default (str): Default coefficients to be used if no mode has been specified. Default: "nominal". fallback (str): Fallback coefficients if the desired coefficients @@ -549,6 +551,18 @@ def __init__(self, coefs, modes=None, default="nominal", fallback=None): raise KeyError("Need at least default coefficients") if self.fallback and self.fallback not in self.coefs: raise KeyError("No fallback coefficients") + self.modes = self._make_modes(modes) + + def _make_modes(self, modes): + if modes is None: + return {} + elif self._same_mode_for_all_channels(modes): + all_channels = self.coefs[self.default].keys() + return {modes: all_channels} + return modes + + def _same_mode_for_all_channels(self, modes): + return isinstance(modes, str) def get_coefs(self, channel): """Get calibration coefficients for the given channel. diff --git a/satpy/tests/reader_tests/test_utils.py b/satpy/tests/reader_tests/test_utils.py index fd400bde08..668d6aaf9a 100644 --- a/satpy/tests/reader_tests/test_utils.py +++ b/satpy/tests/reader_tests/test_utils.py @@ -541,6 +541,10 @@ def fixture_coefs(self): None, {"ch1": "nominal_ch1", "ch2": "nominal_ch2"} ), + ( + "nominal", + {"ch1": "nominal_ch1", "ch2": "nominal_ch2"} + ), ( {"nominal": ["ch1", "ch2"]}, {"ch1": "nominal_ch1", "ch2": "nominal_ch2"} From 02607032dc05fedd3a9c1133c7856c898d0f555e Mon Sep 17 00:00:00 2001 From: Stephan Finkensieper Date: Thu, 13 Jun 2024 09:37:16 +0000 Subject: [PATCH 07/13] Add coefficient parser --- satpy/readers/utils.py | 62 ++++++++++++++++++++++ satpy/tests/reader_tests/test_utils.py | 73 +++++++++++++++++++++++--- 2 files changed, 129 insertions(+), 6 deletions(-) diff --git a/satpy/readers/utils.py b/satpy/readers/utils.py index 4a83c26f16..18288d9708 100644 --- a/satpy/readers/utils.py +++ b/satpy/readers/utils.py @@ -476,6 +476,68 @@ def remove_earthsun_distance_correction(reflectance, utc_date=None): return reflectance +class CalibrationCoefficientParser: + """TODO.""" + def __init__(self, coefs, default="nominal"): + """TODO.""" + self.coefs = coefs + self.default = default + + def parse(self, user_input): + """TODO.""" + if user_input is None: + return self._get_coefs_set(self.default) # FIXME: Does not check missing coefs + elif isinstance(user_input, str): + return self._get_coefs_set(user_input) # FIXME: Does not check missing coefs + elif isinstance(user_input, dict): + return self._expand_user_input(user_input) + raise ValueError(f"Unsupported calibration coefficients. Expected dict/str, got {type(user_input)}") + + def _expand_user_input(self, user_input): + coefs = {} + for channel, mode_or_coefs in user_input.items(): + if self._is_single_channel(channel): + coefs[channel] = self._get_coefs_for_single_channel(mode_or_coefs, channel) + elif self._is_multi_channel(channel): + coefs = self._get_coefs_for_multiple_channels(mode_or_coefs, channel) + coefs.update(coefs) + else: + raise ValueError("TODO") + return coefs + + def _is_single_channel(self, key): + return isinstance(key, str) + + def _get_coefs_for_single_channel(self, mode_or_coefs, channel): + if isinstance(mode_or_coefs, str): + mode = mode_or_coefs + return self._get_coefs(mode, channel) + return mode_or_coefs + + def _is_multi_channel(self, key): + return isinstance(key, tuple) + + def _get_coefs_for_multiple_channels(self, mode, channels): + return {channel: self._get_coefs(mode, channel) for channel in channels} + + def _get_coefs(self, mode, channel): + coefs_set = self._get_coefs_set(mode) + return self._get_coefs_from_set(coefs_set, channel, mode) + + def _get_coefs_set(self, mode): + try: + return self.coefs[mode] + except KeyError: + modes = list(self.coefs.keys()) + raise KeyError(f"Unknown calibration mode: {mode}. Choose one of {modes}") + + def _get_coefs_from_set(self, coefs_set, channel, mode): + try: + return coefs_set[channel] + except KeyError: + raise KeyError(f"No {mode} calibration coefficients for {channel}") + + class CalibrationCoefficientSelector: """Helper for choosing coefficients out of multiple options. diff --git a/satpy/tests/reader_tests/test_utils.py b/satpy/tests/reader_tests/test_utils.py index 668d6aaf9a..ca958faab1 100644 --- a/satpy/tests/reader_tests/test_utils.py +++ b/satpy/tests/reader_tests/test_utils.py @@ -34,7 +34,6 @@ from satpy.readers import FSFile from satpy.readers import utils as hf -from satpy.readers.utils import CalibrationCoefficientSelector class TestHelpers(unittest.TestCase): @@ -515,6 +514,68 @@ def test_generic_open_binary(tmp_path, data, filename, mode): assert read_binary_data == dummy_data +class TestCalibrationCoefficientParser: + """TODO.""" + @pytest.fixture(name="coefs") + def fixture_coefs(self): + """Get fake coefficients.""" + return { + "nominal": { + "ch1": "nominal_ch1", + "ch2": "nominal_ch2" + }, + "mode1": { + "ch1": "mode1_ch1", + }, + "mode2": { + "ch2": "mode2_ch2", + } + } + + @pytest.mark.parametrize( + ("user_input", "expected"), + [ + (None, {"ch1": "nominal_ch1", "ch2": "nominal_ch2"}), + ("nominal", {"ch1": "nominal_ch1", "ch2": "nominal_ch2"}), + ( + {("ch1", "ch2"): "nominal"}, + {"ch1": "nominal_ch1", "ch2": "nominal_ch2"} + ), + ( + {"ch1": "mode1", "ch2": "mode2"}, + {"ch1": "mode1_ch1", "ch2": "mode2_ch2"} + ), + ( + {"ch1": "mode1", "ch2": {"gain": 1}}, + {"ch1": "mode1_ch1", "ch2": {"gain": 1}} + ), + ] + ) + def test_parse(self, coefs, user_input, expected): + """TODO.""" + s = hf.CalibrationCoefficientParser(coefs) + coefs = s.parse(user_input) + assert coefs == expected + + @pytest.mark.parametrize( + "user_input", ["foo", {"ch1": "foo"}, {("ch1", "ch2"): "foo"}] + ) + def test_missing_mode(self, coefs, user_input): + """TODO.""" + s = hf.CalibrationCoefficientParser(coefs) + with pytest.raises(KeyError, match="Unknown calibration mode *"): + s.parse(user_input) + + @pytest.mark.parametrize( + "user_input", [{"ch2": "mode1"}, {("ch1", "ch2"): "mode1"}] + ) + def test_missing_coefs(self, coefs, user_input): + """TODO.""" + s = hf.CalibrationCoefficientParser(coefs) + with pytest.raises(KeyError, match="No mode1 calibration *"): + s.parse(user_input) + + class TestCalibrationCoefficientSelector: """Test selection of calibration coefficients.""" @@ -561,7 +622,7 @@ def fixture_coefs(self): ) def test_get_coefs(self, coefs, calib_modes, expected): """Test getting calibration coefficients.""" - s = CalibrationCoefficientSelector(coefs, calib_modes) + s = hf.CalibrationCoefficientSelector(coefs, calib_modes) coefs = { channel: s.get_coefs(channel) for channel in ["ch1", "ch2"] @@ -571,22 +632,22 @@ def test_get_coefs(self, coefs, calib_modes, expected): def test_missing_coefs(self, coefs): """Test handling of missing coefficients.""" calib_modes = {"mode2": ["ch1"]} - s = CalibrationCoefficientSelector(coefs, calib_modes) + s = hf.CalibrationCoefficientSelector(coefs, calib_modes) with pytest.raises(KeyError, match="No mode2 calibration *"): s.get_coefs("ch1") def test_fallback_to_nominal(self, coefs): """Test falling back to nominal coefficients.""" calib_modes = {"mode2": ["ch1"]} - s = CalibrationCoefficientSelector(coefs, calib_modes, fallback="nominal") + s = hf.CalibrationCoefficientSelector(coefs, calib_modes, fallback="nominal") assert s.get_coefs("ch1") == "nominal_ch1" def test_no_default_coefs(self): """Test initialization without default coefficients.""" with pytest.raises(KeyError, match="Need at least *"): - CalibrationCoefficientSelector({}) + hf.CalibrationCoefficientSelector({}) def test_no_fallback(self): """Test initialization without fallback coefficients.""" with pytest.raises(KeyError, match="No fallback coefficients"): - CalibrationCoefficientSelector({"nominal": 123}, fallback="foo") + hf.CalibrationCoefficientSelector({"nominal": 123}, fallback="foo") From 4227be75b461068fe6e9861ed1890476502eeb5f Mon Sep 17 00:00:00 2001 From: Stephan Finkensieper Date: Thu, 13 Jun 2024 12:14:19 +0000 Subject: [PATCH 08/13] Refactor parsing --- satpy/readers/utils.py | 115 ++++++++++++----------- satpy/tests/reader_tests/test_utils.py | 123 ++++++++----------------- 2 files changed, 95 insertions(+), 143 deletions(-) diff --git a/satpy/readers/utils.py b/satpy/readers/utils.py index 18288d9708..c52aca475b 100644 --- a/satpy/readers/utils.py +++ b/satpy/readers/utils.py @@ -476,34 +476,47 @@ def remove_earthsun_distance_correction(reflectance, utc_date=None): return reflectance -class CalibrationCoefficientParser: - """TODO.""" +class _CalibrationCoefficientParser: + """Parse user-defined calibration coefficients.""" + def __init__(self, coefs, default="nominal"): """TODO.""" + if default not in coefs: + raise KeyError("Need at least default coefficients") self.coefs = coefs self.default = default - def parse(self, user_input): - """TODO.""" - if user_input is None: - return self._get_coefs_set(self.default) # FIXME: Does not check missing coefs - elif isinstance(user_input, str): - return self._get_coefs_set(user_input) # FIXME: Does not check missing coefs - elif isinstance(user_input, dict): - return self._expand_user_input(user_input) - raise ValueError(f"Unsupported calibration coefficients. Expected dict/str, got {type(user_input)}") - - def _expand_user_input(self, user_input): - coefs = {} - for channel, mode_or_coefs in user_input.items(): - if self._is_single_channel(channel): - coefs[channel] = self._get_coefs_for_single_channel(mode_or_coefs, channel) - elif self._is_multi_channel(channel): - coefs = self._get_coefs_for_multiple_channels(mode_or_coefs, channel) - coefs.update(coefs) + def parse(self, wishlist): + """Parse user-defined calibration coefficients.""" + if wishlist is None: + return self._get_coefs_set(self.default) + elif isinstance(wishlist, str): + return self._get_coefs_set(wishlist) + elif isinstance(wishlist, dict): + return self._parse(wishlist) + raise TypeError(f"Unsupported wishlist type. Expected dict/str, got {type(wishlist)}") + + def _parse(self, wishlist): + wishlist = self._expand_multi_channel_keys(wishlist) + return self._replace_calib_mode_with_actual_coefs(wishlist) + + def _expand_multi_channel_keys(self, wishlist): + expanded = {} + for channels, coefs in wishlist.items(): + if self._is_multi_channel(channels): + for channel in channels: + expanded[channel] = coefs else: - raise ValueError("TODO") - return coefs + expanded[channels] = coefs + return expanded + + def _replace_calib_mode_with_actual_coefs(self, wishlist): + res = {} + for channel, mode_or_coefs in wishlist.items(): + coefs = self._get_coefs_for_single_channel(mode_or_coefs, channel) + if coefs: + res[channel] = coefs + return res def _is_single_channel(self, key): return isinstance(key, str) @@ -518,11 +531,16 @@ def _is_multi_channel(self, key): return isinstance(key, tuple) def _get_coefs_for_multiple_channels(self, mode, channels): - return {channel: self._get_coefs(mode, channel) for channel in channels} + res = {} + for channel in channels: + coefs = self._get_coefs(mode, channel) + if coefs is not None: + res[channel] = coefs + return res def _get_coefs(self, mode, channel): coefs_set = self._get_coefs_set(mode) - return self._get_coefs_from_set(coefs_set, channel, mode) + return coefs_set.get(channel, None) def _get_coefs_set(self, mode): try: @@ -531,11 +549,13 @@ def _get_coefs_set(self, mode): modes = list(self.coefs.keys()) raise KeyError(f"Unknown calibration mode: {mode}. Choose one of {modes}") - def _get_coefs_from_set(self, coefs_set, channel, mode): - try: - return coefs_set[channel] - except KeyError: - raise KeyError(f"No {mode} calibration coefficients for {channel}") + def get_calib_mode(self, wishlist, channel): + """Get desired calibration mode for the given channel.""" + if isinstance(wishlist, str): + return wishlist + elif isinstance(wishlist, dict): + expanded = self._expand_multi_channel_keys(wishlist) + return expanded[channel] class CalibrationCoefficientSelector: @@ -590,7 +610,7 @@ class CalibrationCoefficientSelector: "nominal_ch3" """ - def __init__(self, coefs, modes=None, default="nominal", fallback=None): + def __init__(self, coefs, wishlist, default="nominal", fallback=None): """Initialize the coefficient selector. Args: @@ -605,26 +625,14 @@ def __init__(self, coefs, modes=None, default="nominal", fallback=None): fallback (str): Fallback coefficients if the desired coefficients are not available for some channel. """ + if fallback and fallback not in coefs: + raise KeyError("No fallback coefficients") self.coefs = coefs - self.modes = modes or {} + self.wishlist = wishlist self.default = default self.fallback = fallback - if self.default not in self.coefs: - raise KeyError("Need at least default coefficients") - if self.fallback and self.fallback not in self.coefs: - raise KeyError("No fallback coefficients") - self.modes = self._make_modes(modes) - - def _make_modes(self, modes): - if modes is None: - return {} - elif self._same_mode_for_all_channels(modes): - all_channels = self.coefs[self.default].keys() - return {modes: all_channels} - return modes - - def _same_mode_for_all_channels(self, modes): - return isinstance(modes, str) + self.parser = _CalibrationCoefficientParser(coefs, default) + self.parsed_wishlist = self.parser.parse(wishlist) def get_coefs(self, channel): """Get calibration coefficients for the given channel. @@ -632,19 +640,10 @@ def get_coefs(self, channel): Args: channel (str): Channel name """ - mode = self._get_mode(channel) - return self._get_coefs(channel, mode) - - def _get_coefs(self, channel, mode): try: - return self.coefs[mode][channel] + return self.parsed_wishlist[channel] except KeyError: if self.fallback: return self.coefs[self.fallback][channel] + mode = self.parser.get_calib_mode(self.wishlist, channel) raise KeyError(f"No {mode} calibration coefficients for {channel}") - - def _get_mode(self, channel): - for mode, channels in self.modes.items(): - if channel in channels: - return mode - return self.default diff --git a/satpy/tests/reader_tests/test_utils.py b/satpy/tests/reader_tests/test_utils.py index ca958faab1..4d4d5871f9 100644 --- a/satpy/tests/reader_tests/test_utils.py +++ b/satpy/tests/reader_tests/test_utils.py @@ -514,8 +514,9 @@ def test_generic_open_binary(tmp_path, data, filename, mode): assert read_binary_data == dummy_data -class TestCalibrationCoefficientParser: - """TODO.""" +class TestCalibrationCoefficientSelector: + """Unit tests for calibration coefficient selection.""" + @pytest.fixture(name="coefs") def fixture_coefs(self): """Get fake coefficients.""" @@ -533,7 +534,7 @@ def fixture_coefs(self): } @pytest.mark.parametrize( - ("user_input", "expected"), + ("wishlist", "expected"), [ (None, {"ch1": "nominal_ch1", "ch2": "nominal_ch2"}), ("nominal", {"ch1": "nominal_ch1", "ch2": "nominal_ch2"}), @@ -541,6 +542,10 @@ def fixture_coefs(self): {("ch1", "ch2"): "nominal"}, {"ch1": "nominal_ch1", "ch2": "nominal_ch2"} ), + ( + {"ch1": "mode1"}, + {"ch1": "mode1_ch1", "ch2": "nominal_ch2"} + ), ( {"ch1": "mode1", "ch2": "mode2"}, {"ch1": "mode1_ch1", "ch2": "mode2_ch2"} @@ -551,103 +556,51 @@ def fixture_coefs(self): ), ] ) - def test_parse(self, coefs, user_input, expected): - """TODO.""" - s = hf.CalibrationCoefficientParser(coefs) - coefs = s.parse(user_input) + def test_get_coefs(self, coefs, wishlist, expected): + """Test getting calibration coefficients.""" + s = hf.CalibrationCoefficientSelector(coefs, wishlist) + coefs = { + channel: s.get_coefs(channel) + for channel in ["ch1", "ch2"] + } assert coefs == expected @pytest.mark.parametrize( - "user_input", ["foo", {"ch1": "foo"}, {("ch1", "ch2"): "foo"}] + "wishlist", ["foo", {"ch1": "foo"}, {("ch1", "ch2"): "foo"}] ) - def test_missing_mode(self, coefs, user_input): - """TODO.""" - s = hf.CalibrationCoefficientParser(coefs) - with pytest.raises(KeyError, match="Unknown calibration mode *"): - s.parse(user_input) + def test_unknown_mode(self, coefs, wishlist): + """Test handling of unknown calibration mode.""" + with pytest.raises(KeyError, match="Unknown calibration mode"): + hf.CalibrationCoefficientSelector(coefs, wishlist) @pytest.mark.parametrize( - "user_input", [{"ch2": "mode1"}, {("ch1", "ch2"): "mode1"}] + "wishlist", ["mode1", {"ch2": "mode1"}, {("ch1", "ch2"): "mode1"}] ) - def test_missing_coefs(self, coefs, user_input): - """TODO.""" - s = hf.CalibrationCoefficientParser(coefs) - with pytest.raises(KeyError, match="No mode1 calibration *"): - s.parse(user_input) - - -class TestCalibrationCoefficientSelector: - """Test selection of calibration coefficients.""" - - @pytest.fixture(name="coefs") - def fixture_coefs(self): - """Get fake coefficients.""" - return { - "nominal": { - "ch1": "nominal_ch1", - "ch2": "nominal_ch2" - }, - "mode1": { - "ch1": "mode1_ch1", - }, - "mode2": { - "ch2": "mode2_ch2", - } - } + def test_missing_coefs(self, coefs, wishlist): + """Test that an exception is raised when coefficients are missing.""" + s = hf.CalibrationCoefficientSelector(coefs, wishlist) + with pytest.raises(KeyError, match="No mode1 calibration"): + s.get_coefs("ch2") @pytest.mark.parametrize( - ("calib_modes", "expected"), - [ - ( - None, - {"ch1": "nominal_ch1", "ch2": "nominal_ch2"} - ), - ( - "nominal", - {"ch1": "nominal_ch1", "ch2": "nominal_ch2"} - ), - ( - {"nominal": ["ch1", "ch2"]}, - {"ch1": "nominal_ch1", "ch2": "nominal_ch2"} - ), - ( - {"mode1": ["ch1"]}, - {"ch1": "mode1_ch1", "ch2": "nominal_ch2"} - ), - ( - {"mode1": ["ch1"], "mode2": ["ch2"]}, - {"ch1": "mode1_ch1", "ch2": "mode2_ch2"} - ), - ] + "wishlist", ["mode1", {"ch2": "mode1"}, {("ch1", "ch2"): "mode1"}] ) - def test_get_coefs(self, coefs, calib_modes, expected): - """Test getting calibration coefficients.""" - s = hf.CalibrationCoefficientSelector(coefs, calib_modes) - coefs = { - channel: s.get_coefs(channel) - for channel in ["ch1", "ch2"] - } - assert coefs == expected - - def test_missing_coefs(self, coefs): - """Test handling of missing coefficients.""" - calib_modes = {"mode2": ["ch1"]} - s = hf.CalibrationCoefficientSelector(coefs, calib_modes) - with pytest.raises(KeyError, match="No mode2 calibration *"): - s.get_coefs("ch1") - - def test_fallback_to_nominal(self, coefs): + def test_fallback_to_nominal(self, coefs, wishlist): """Test falling back to nominal coefficients.""" - calib_modes = {"mode2": ["ch1"]} - s = hf.CalibrationCoefficientSelector(coefs, calib_modes, fallback="nominal") - assert s.get_coefs("ch1") == "nominal_ch1" + s = hf.CalibrationCoefficientSelector(coefs, wishlist, fallback="nominal") + assert s.get_coefs("ch2") == "nominal_ch2" def test_no_default_coefs(self): """Test initialization without default coefficients.""" - with pytest.raises(KeyError, match="Need at least *"): - hf.CalibrationCoefficientSelector({}) + with pytest.raises(KeyError, match="Need at least"): + hf.CalibrationCoefficientSelector({}, {}) def test_no_fallback(self): """Test initialization without fallback coefficients.""" with pytest.raises(KeyError, match="No fallback coefficients"): - hf.CalibrationCoefficientSelector({"nominal": 123}, fallback="foo") + hf.CalibrationCoefficientSelector({"nominal": 123}, {}, fallback="foo") + + def test_invalid_wishlist_type(self): + """Test handling of invalid wishlist type.""" + with pytest.raises(TypeError, match="Unsupported wishlist type"): + hf.CalibrationCoefficientSelector({"nominal": 123}, 123) From 039ef691268727f45018b229ca964ad9ff4ef890 Mon Sep 17 00:00:00 2001 From: Stephan Finkensieper Date: Thu, 13 Jun 2024 12:40:49 +0000 Subject: [PATCH 09/13] Update documentation example --- satpy/readers/utils.py | 93 ++++++++++++++++++++++-------------------- 1 file changed, 48 insertions(+), 45 deletions(-) diff --git a/satpy/readers/utils.py b/satpy/readers/utils.py index c52aca475b..67174b00e9 100644 --- a/satpy/readers/utils.py +++ b/satpy/readers/utils.py @@ -480,7 +480,7 @@ class _CalibrationCoefficientParser: """Parse user-defined calibration coefficients.""" def __init__(self, coefs, default="nominal"): - """TODO.""" + """Initialize the parser.""" if default not in coefs: raise KeyError("Need at least default coefficients") self.coefs = coefs @@ -493,52 +493,43 @@ def parse(self, wishlist): elif isinstance(wishlist, str): return self._get_coefs_set(wishlist) elif isinstance(wishlist, dict): - return self._parse(wishlist) + return self._parse_dict(wishlist) raise TypeError(f"Unsupported wishlist type. Expected dict/str, got {type(wishlist)}") - def _parse(self, wishlist): - wishlist = self._expand_multi_channel_keys(wishlist) + def _parse_dict(self, wishlist): + wishlist = self._flatten_multi_channel_keys(wishlist) return self._replace_calib_mode_with_actual_coefs(wishlist) - def _expand_multi_channel_keys(self, wishlist): - expanded = {} + def _flatten_multi_channel_keys(self, wishlist): + flat = {} for channels, coefs in wishlist.items(): if self._is_multi_channel(channels): - for channel in channels: - expanded[channel] = coefs + flat.update({channel: coefs for channel in channels}) else: - expanded[channels] = coefs - return expanded + flat[channels] = coefs + return flat + + def _is_multi_channel(self, key): + return isinstance(key, tuple) def _replace_calib_mode_with_actual_coefs(self, wishlist): res = {} - for channel, mode_or_coefs in wishlist.items(): - coefs = self._get_coefs_for_single_channel(mode_or_coefs, channel) + for channel in self.coefs[self.default]: + mode_or_coefs = wishlist.get(channel, self.default) + coefs = self._get_coefs(mode_or_coefs, channel) if coefs: res[channel] = coefs return res - def _is_single_channel(self, key): - return isinstance(key, str) - - def _get_coefs_for_single_channel(self, mode_or_coefs, channel): - if isinstance(mode_or_coefs, str): - mode = mode_or_coefs - return self._get_coefs(mode, channel) + def _get_coefs(self, mode_or_coefs, channel): + if self._is_mode(mode_or_coefs): + return self._get_coefs_by_mode(mode_or_coefs, channel) return mode_or_coefs - def _is_multi_channel(self, key): - return isinstance(key, tuple) - - def _get_coefs_for_multiple_channels(self, mode, channels): - res = {} - for channel in channels: - coefs = self._get_coefs(mode, channel) - if coefs is not None: - res[channel] = coefs - return res + def _is_mode(self, mode_or_coefs): + return isinstance(mode_or_coefs, str) - def _get_coefs(self, mode, channel): + def _get_coefs_by_mode(self, mode, channel): coefs_set = self._get_coefs_set(mode) return coefs_set.get(channel, None) @@ -554,17 +545,21 @@ def get_calib_mode(self, wishlist, channel): if isinstance(wishlist, str): return wishlist elif isinstance(wishlist, dict): - expanded = self._expand_multi_channel_keys(wishlist) - return expanded[channel] + flat = self._flatten_multi_channel_keys(wishlist) + return flat[channel] class CalibrationCoefficientSelector: """Helper for choosing coefficients out of multiple options. Example: Three sets of coefficients are available (nominal, meirink, gsics). - Calibrate channel 1 with "meirink" and channels 2/3 with "gsics". + A user wants to calibrate + - channel 1 with "meirink" + - channels 2/3 with "gsics" + - channel 4 with custom coefficients + - remaining channels with nominal coefficients - 1. Setup + 1. Define coefficients .. code-block:: python @@ -575,6 +570,8 @@ class CalibrationCoefficientSelector: "ch1": "nominal_ch1", "ch2": "nominal_ch2", "ch3": "nominal_ch3" + "ch4": "nominal_ch4" + "ch5": "nominal_ch5" }, "meirink": { "ch1": "meirink_ch1", @@ -584,28 +581,33 @@ class CalibrationCoefficientSelector: # ch3 coefficients are missing } } - modes = { - "meirink": ["ch1"], - "gsics": ["ch2", "ch3"] + wishlist = { + "ch1": "meirink", + ("ch2", "ch3"): "gsics" + "ch4": {"mygain": 123}, } 2. Query: .. code-block:: python - >>> s = CalibrationCoefficientSelector(coefs, modes) + >>> s = CalibrationCoefficientSelector(coefs, wishlist) >>> s.get_coefs("ch1") "meirink_ch1" >>> s.get_coefs("ch2") "gsics_ch2" >>> s.get_coefs("ch3") KeyError: 'No gsics calibration coefficients for ch3' + >>> s.get_coefs("ch4") + {"mygain": 123} + >>> s.get_coefs("ch5") + "nominal_ch5 3. Fallback to nominal for ch3: .. code-block:: python - >>> s = CalibrationCoefficientSelector(coefs, modes, fallback="nominal") + >>> s = CalibrationCoefficientSelector(coefs, wishlist, fallback="nominal") >>> s.get_coefs("ch3") "nominal_ch3" """ @@ -617,13 +619,14 @@ def __init__(self, coefs, wishlist, default="nominal", fallback=None): coefs (dict): One set of calibration coefficients for each calibration mode. The actual coefficients can be of any type (reader-specific). - modes (str or dict): Desired calibration modes. Use a dictionary - `{mode: channels}` to specify multiple modes. Use a string to - specify one mode for all channels. - default (str): Default coefficients to be used if no mode has been - specified. Default: "nominal". + wishlist (str or dict): Desired calibration coefficients. Use a + dictionary to specify channel-specific coefficients. Use a + string to specify one mode for all channels. + default (str): Default coefficients to be used if nothing was + specified in the wishlist. Default: "nominal". fallback (str): Fallback coefficients if the desired coefficients - are not available for some channel. + are not available for some channel. By default, an exception is + raised if coefficients are missing. """ if fallback and fallback not in coefs: raise KeyError("No fallback coefficients") From a2ed4bd0bf93c6b41ea7bda2f43382cdf8349872 Mon Sep 17 00:00:00 2001 From: Stephan Finkensieper Date: Fri, 14 Jun 2024 07:53:33 +0000 Subject: [PATCH 10/13] Rename wishlist --- satpy/readers/utils.py | 67 ++++++++++++++------------ satpy/tests/reader_tests/test_utils.py | 5 +- 2 files changed, 40 insertions(+), 32 deletions(-) diff --git a/satpy/readers/utils.py b/satpy/readers/utils.py index 67174b00e9..910dbe8e06 100644 --- a/satpy/readers/utils.py +++ b/satpy/readers/utils.py @@ -486,23 +486,26 @@ def __init__(self, coefs, default="nominal"): self.coefs = coefs self.default = default - def parse(self, wishlist): - """Parse user-defined calibration coefficients.""" - if wishlist is None: + def parse(self, calib_wishlist): + """Parse user's calibration wishlist.""" + if calib_wishlist is None: return self._get_coefs_set(self.default) - elif isinstance(wishlist, str): - return self._get_coefs_set(wishlist) - elif isinstance(wishlist, dict): - return self._parse_dict(wishlist) - raise TypeError(f"Unsupported wishlist type. Expected dict/str, got {type(wishlist)}") + elif isinstance(calib_wishlist, str): + return self._get_coefs_set(calib_wishlist) + elif isinstance(calib_wishlist, dict): + return self._parse_dict(calib_wishlist) + raise TypeError( + f"Unsupported wishlist type. Expected dict/str, " + f"got {type(calib_wishlist)}" + ) - def _parse_dict(self, wishlist): - wishlist = self._flatten_multi_channel_keys(wishlist) - return self._replace_calib_mode_with_actual_coefs(wishlist) + def _parse_dict(self, calib_wishlist): + calib_wishlist = self._flatten_multi_channel_keys(calib_wishlist) + return self._replace_calib_mode_with_actual_coefs(calib_wishlist) - def _flatten_multi_channel_keys(self, wishlist): + def _flatten_multi_channel_keys(self, calib_wishlist): flat = {} - for channels, coefs in wishlist.items(): + for channels, coefs in calib_wishlist.items(): if self._is_multi_channel(channels): flat.update({channel: coefs for channel in channels}) else: @@ -512,10 +515,10 @@ def _flatten_multi_channel_keys(self, wishlist): def _is_multi_channel(self, key): return isinstance(key, tuple) - def _replace_calib_mode_with_actual_coefs(self, wishlist): + def _replace_calib_mode_with_actual_coefs(self, calib_wishlist): res = {} for channel in self.coefs[self.default]: - mode_or_coefs = wishlist.get(channel, self.default) + mode_or_coefs = calib_wishlist.get(channel, self.default) coefs = self._get_coefs(mode_or_coefs, channel) if coefs: res[channel] = coefs @@ -540,12 +543,12 @@ def _get_coefs_set(self, mode): modes = list(self.coefs.keys()) raise KeyError(f"Unknown calibration mode: {mode}. Choose one of {modes}") - def get_calib_mode(self, wishlist, channel): + def get_calib_mode(self, calib_wishlist, channel): """Get desired calibration mode for the given channel.""" - if isinstance(wishlist, str): - return wishlist - elif isinstance(wishlist, dict): - flat = self._flatten_multi_channel_keys(wishlist) + if isinstance(calib_wishlist, str): + return calib_wishlist + elif isinstance(calib_wishlist, dict): + flat = self._flatten_multi_channel_keys(calib_wishlist) return flat[channel] @@ -581,7 +584,7 @@ class CalibrationCoefficientSelector: # ch3 coefficients are missing } } - wishlist = { + calib_wishlist = { "ch1": "meirink", ("ch2", "ch3"): "gsics" "ch4": {"mygain": 123}, @@ -591,7 +594,7 @@ class CalibrationCoefficientSelector: .. code-block:: python - >>> s = CalibrationCoefficientSelector(coefs, wishlist) + >>> s = CalibrationCoefficientSelector(coefs, calib_wishlist) >>> s.get_coefs("ch1") "meirink_ch1" >>> s.get_coefs("ch2") @@ -607,35 +610,35 @@ class CalibrationCoefficientSelector: .. code-block:: python - >>> s = CalibrationCoefficientSelector(coefs, wishlist, fallback="nominal") + >>> s = CalibrationCoefficientSelector(coefs, calib_wishlist, fallback="nominal") >>> s.get_coefs("ch3") "nominal_ch3" """ - def __init__(self, coefs, wishlist, default="nominal", fallback=None): + def __init__(self, coefs, calib_wishlist, default="nominal", fallback=None): """Initialize the coefficient selector. Args: coefs (dict): One set of calibration coefficients for each calibration mode. The actual coefficients can be of any type (reader-specific). - wishlist (str or dict): Desired calibration coefficients. Use a + calib_wishlist (str or dict): Desired calibration coefficients. Use a dictionary to specify channel-specific coefficients. Use a string to specify one mode for all channels. default (str): Default coefficients to be used if nothing was - specified in the wishlist. Default: "nominal". + specified in the calib_wishlist. Default: "nominal". fallback (str): Fallback coefficients if the desired coefficients are not available for some channel. By default, an exception is raised if coefficients are missing. """ if fallback and fallback not in coefs: - raise KeyError("No fallback coefficients") + raise KeyError("No fallback calibration coefficients") self.coefs = coefs - self.wishlist = wishlist + self.calib_wishlist = calib_wishlist self.default = default self.fallback = fallback self.parser = _CalibrationCoefficientParser(coefs, default) - self.parsed_wishlist = self.parser.parse(wishlist) + self.parsed_wishlist = self.parser.parse(calib_wishlist) def get_coefs(self, channel): """Get calibration coefficients for the given channel. @@ -646,7 +649,11 @@ def get_coefs(self, channel): try: return self.parsed_wishlist[channel] except KeyError: + mode = self.parser.get_calib_mode(self.calib_wishlist, channel) if self.fallback: + LOGGER.warning( + f"No {mode} calibration coefficients for {channel}. " + f"Falling back to {self.fallback}." + ) return self.coefs[self.fallback][channel] - mode = self.parser.get_calib_mode(self.wishlist, channel) raise KeyError(f"No {mode} calibration coefficients for {channel}") diff --git a/satpy/tests/reader_tests/test_utils.py b/satpy/tests/reader_tests/test_utils.py index 4d4d5871f9..79da09f095 100644 --- a/satpy/tests/reader_tests/test_utils.py +++ b/satpy/tests/reader_tests/test_utils.py @@ -585,10 +585,11 @@ def test_missing_coefs(self, coefs, wishlist): @pytest.mark.parametrize( "wishlist", ["mode1", {"ch2": "mode1"}, {("ch1", "ch2"): "mode1"}] ) - def test_fallback_to_nominal(self, coefs, wishlist): + def test_fallback_to_nominal(self, coefs, wishlist, caplog): """Test falling back to nominal coefficients.""" s = hf.CalibrationCoefficientSelector(coefs, wishlist, fallback="nominal") assert s.get_coefs("ch2") == "nominal_ch2" + assert "Falling back" in caplog.text def test_no_default_coefs(self): """Test initialization without default coefficients.""" @@ -597,7 +598,7 @@ def test_no_default_coefs(self): def test_no_fallback(self): """Test initialization without fallback coefficients.""" - with pytest.raises(KeyError, match="No fallback coefficients"): + with pytest.raises(KeyError, match="No fallback calibration"): hf.CalibrationCoefficientSelector({"nominal": 123}, {}, fallback="foo") def test_invalid_wishlist_type(self): From 8cee9ef3dd4c7586cf335da39588ce1a521e0cb3 Mon Sep 17 00:00:00 2001 From: Stephan Finkensieper Date: Fri, 14 Jun 2024 07:56:57 +0000 Subject: [PATCH 11/13] Rename selector to picker --- satpy/readers/utils.py | 22 +++++++++++----------- satpy/tests/reader_tests/test_utils.py | 23 ++++++++++++----------- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/satpy/readers/utils.py b/satpy/readers/utils.py index 910dbe8e06..e613e903de 100644 --- a/satpy/readers/utils.py +++ b/satpy/readers/utils.py @@ -552,7 +552,7 @@ def get_calib_mode(self, calib_wishlist, channel): return flat[channel] -class CalibrationCoefficientSelector: +class CalibrationCoefficientPicker: """Helper for choosing coefficients out of multiple options. Example: Three sets of coefficients are available (nominal, meirink, gsics). @@ -566,7 +566,7 @@ class CalibrationCoefficientSelector: .. code-block:: python - from satpy.readers.utils import CalibrationCoefficientSelector + from satpy.readers.utils import CalibrationCoefficientPicker coefs = { "nominal": { @@ -594,29 +594,29 @@ class CalibrationCoefficientSelector: .. code-block:: python - >>> s = CalibrationCoefficientSelector(coefs, calib_wishlist) - >>> s.get_coefs("ch1") + >>> picker = CalibrationCoefficientPicker(coefs, calib_wishlist) + >>> picker.get_coefs("ch1") "meirink_ch1" - >>> s.get_coefs("ch2") + >>> picker.get_coefs("ch2") "gsics_ch2" - >>> s.get_coefs("ch3") + >>> picker.get_coefs("ch3") KeyError: 'No gsics calibration coefficients for ch3' - >>> s.get_coefs("ch4") + >>> picker.get_coefs("ch4") {"mygain": 123} - >>> s.get_coefs("ch5") + >>> picker.get_coefs("ch5") "nominal_ch5 3. Fallback to nominal for ch3: .. code-block:: python - >>> s = CalibrationCoefficientSelector(coefs, calib_wishlist, fallback="nominal") - >>> s.get_coefs("ch3") + >>> picker = CalibrationCoefficientPicker(coefs, calib_wishlist, fallback="nominal") + >>> picker.get_coefs("ch3") "nominal_ch3" """ def __init__(self, coefs, calib_wishlist, default="nominal", fallback=None): - """Initialize the coefficient selector. + """Initialize the coefficient picker. Args: coefs (dict): One set of calibration coefficients for each diff --git a/satpy/tests/reader_tests/test_utils.py b/satpy/tests/reader_tests/test_utils.py index 79da09f095..f0561325ce 100644 --- a/satpy/tests/reader_tests/test_utils.py +++ b/satpy/tests/reader_tests/test_utils.py @@ -514,7 +514,7 @@ def test_generic_open_binary(tmp_path, data, filename, mode): assert read_binary_data == dummy_data -class TestCalibrationCoefficientSelector: +class TestCalibrationCoefficientPicker: """Unit tests for calibration coefficient selection.""" @pytest.fixture(name="coefs") @@ -558,9 +558,9 @@ def fixture_coefs(self): ) def test_get_coefs(self, coefs, wishlist, expected): """Test getting calibration coefficients.""" - s = hf.CalibrationCoefficientSelector(coefs, wishlist) + picker = hf.CalibrationCoefficientPicker(coefs, wishlist) coefs = { - channel: s.get_coefs(channel) + channel: picker.get_coefs(channel) for channel in ["ch1", "ch2"] } assert coefs == expected @@ -571,37 +571,38 @@ def test_get_coefs(self, coefs, wishlist, expected): def test_unknown_mode(self, coefs, wishlist): """Test handling of unknown calibration mode.""" with pytest.raises(KeyError, match="Unknown calibration mode"): - hf.CalibrationCoefficientSelector(coefs, wishlist) + hf.CalibrationCoefficientPicker(coefs, wishlist) @pytest.mark.parametrize( "wishlist", ["mode1", {"ch2": "mode1"}, {("ch1", "ch2"): "mode1"}] ) def test_missing_coefs(self, coefs, wishlist): """Test that an exception is raised when coefficients are missing.""" - s = hf.CalibrationCoefficientSelector(coefs, wishlist) + picker = hf.CalibrationCoefficientPicker(coefs, wishlist) with pytest.raises(KeyError, match="No mode1 calibration"): - s.get_coefs("ch2") + picker.get_coefs("ch2") @pytest.mark.parametrize( "wishlist", ["mode1", {"ch2": "mode1"}, {("ch1", "ch2"): "mode1"}] ) def test_fallback_to_nominal(self, coefs, wishlist, caplog): """Test falling back to nominal coefficients.""" - s = hf.CalibrationCoefficientSelector(coefs, wishlist, fallback="nominal") - assert s.get_coefs("ch2") == "nominal_ch2" + picker = hf.CalibrationCoefficientPicker(coefs, wishlist, + fallback="nominal") + assert picker.get_coefs("ch2") == "nominal_ch2" assert "Falling back" in caplog.text def test_no_default_coefs(self): """Test initialization without default coefficients.""" with pytest.raises(KeyError, match="Need at least"): - hf.CalibrationCoefficientSelector({}, {}) + hf.CalibrationCoefficientPicker({}, {}) def test_no_fallback(self): """Test initialization without fallback coefficients.""" with pytest.raises(KeyError, match="No fallback calibration"): - hf.CalibrationCoefficientSelector({"nominal": 123}, {}, fallback="foo") + hf.CalibrationCoefficientPicker({"nominal": 123}, {}, fallback="foo") def test_invalid_wishlist_type(self): """Test handling of invalid wishlist type.""" with pytest.raises(TypeError, match="Unsupported wishlist type"): - hf.CalibrationCoefficientSelector({"nominal": 123}, 123) + hf.CalibrationCoefficientPicker({"nominal": 123}, 123) From 0d54d5478394d5274cc26ec77a9f4a585afba7a1 Mon Sep 17 00:00:00 2001 From: Stephan Finkensieper Date: Fri, 14 Jun 2024 08:36:33 +0000 Subject: [PATCH 12/13] Return calibration modes as well --- satpy/readers/utils.py | 75 ++++++++++++++++---------- satpy/tests/reader_tests/test_utils.py | 47 ++++++++++++---- 2 files changed, 84 insertions(+), 38 deletions(-) diff --git a/satpy/readers/utils.py b/satpy/readers/utils.py index e613e903de..b127de87ee 100644 --- a/satpy/readers/utils.py +++ b/satpy/readers/utils.py @@ -527,7 +527,7 @@ def _replace_calib_mode_with_actual_coefs(self, calib_wishlist): def _get_coefs(self, mode_or_coefs, channel): if self._is_mode(mode_or_coefs): return self._get_coefs_by_mode(mode_or_coefs, channel) - return mode_or_coefs + return _make_coefs(mode_or_coefs, "external") def _is_mode(self, mode_or_coefs): return isinstance(mode_or_coefs, str) @@ -538,7 +538,10 @@ def _get_coefs_by_mode(self, mode, channel): def _get_coefs_set(self, mode): try: - return self.coefs[mode] + return { + channel: _make_coefs(coefs, mode) + for channel, coefs in self.coefs[mode].items() + } except KeyError: modes = list(self.coefs.keys()) raise KeyError(f"Unknown calibration mode: {mode}. Choose one of {modes}") @@ -557,12 +560,25 @@ class CalibrationCoefficientPicker: Example: Three sets of coefficients are available (nominal, meirink, gsics). A user wants to calibrate - - channel 1 with "meirink" - - channels 2/3 with "gsics" - - channel 4 with custom coefficients - - remaining channels with nominal coefficients - 1. Define coefficients + - channel 1 with "meirink" + - channels 2/3 with "gsics" + - channel 4 with custom coefficients + - remaining channels with nominal coefficients + + 1. Users provide a wishlist via ``reader_kwargs`` + + .. code-block:: python + + calib_wishlist = { + "ch1": "meirink", + ("ch2", "ch3"): "gsics" + "ch4": {"mygain": 123}, + } + # Also possible: Same mode for all channels via + # calib_wishlist = "gsics" + + 2. Readers provide a dictionary with all available coefficients .. code-block:: python @@ -570,49 +586,45 @@ class CalibrationCoefficientPicker: coefs = { "nominal": { - "ch1": "nominal_ch1", - "ch2": "nominal_ch2", - "ch3": "nominal_ch3" - "ch4": "nominal_ch4" - "ch5": "nominal_ch5" + "ch1": 1.0, + "ch2": 2.0, + "ch3": 3.0, + "ch4": 4.0, + "ch5": 5.0, }, "meirink": { - "ch1": "meirink_ch1", + "ch1": 1.1, }, "gsics": { - "ch2": "gsics_ch2", + "ch2": 2.2, # ch3 coefficients are missing } } - calib_wishlist = { - "ch1": "meirink", - ("ch2", "ch3"): "gsics" - "ch4": {"mygain": 123}, - } - 2. Query: + 3. Raders make queries to get the desired coefficients: .. code-block:: python >>> picker = CalibrationCoefficientPicker(coefs, calib_wishlist) >>> picker.get_coefs("ch1") - "meirink_ch1" + {"coefs": 1.0, "mode": "meirink"} >>> picker.get_coefs("ch2") - "gsics_ch2" + {"coefs": 2.2, "mode": "gsics"} >>> picker.get_coefs("ch3") KeyError: 'No gsics calibration coefficients for ch3' >>> picker.get_coefs("ch4") - {"mygain": 123} + {"coefs": {"mygain": 123}, "mode": "external"} >>> picker.get_coefs("ch5") - "nominal_ch5 + {"coefs": 5.0, "mode": "nominal"} - 3. Fallback to nominal for ch3: + 4. Fallback to nominal coefficients for ch3: .. code-block:: python >>> picker = CalibrationCoefficientPicker(coefs, calib_wishlist, fallback="nominal") >>> picker.get_coefs("ch3") - "nominal_ch3" + {"coefs": 3.0, "mode": "nominal"} + """ def __init__(self, coefs, calib_wishlist, default="nominal", fallback=None): @@ -645,6 +657,10 @@ def get_coefs(self, channel): Args: channel (str): Channel name + + Returns: + dict: Calibration coefficients and mode (for transparency, in case + the picked coefficients differ from the wishlist). """ try: return self.parsed_wishlist[channel] @@ -655,5 +671,10 @@ def get_coefs(self, channel): f"No {mode} calibration coefficients for {channel}. " f"Falling back to {self.fallback}." ) - return self.coefs[self.fallback][channel] + return _make_coefs(self.coefs[self.fallback][channel], + self.fallback) raise KeyError(f"No {mode} calibration coefficients for {channel}") + + +def _make_coefs(coefs, mode): + return {"coefs": coefs, "mode": mode} diff --git a/satpy/tests/reader_tests/test_utils.py b/satpy/tests/reader_tests/test_utils.py index f0561325ce..b36a2b1d60 100644 --- a/satpy/tests/reader_tests/test_utils.py +++ b/satpy/tests/reader_tests/test_utils.py @@ -522,37 +522,61 @@ def fixture_coefs(self): """Get fake coefficients.""" return { "nominal": { - "ch1": "nominal_ch1", - "ch2": "nominal_ch2" + "ch1": 1.0, + "ch2": 2.0, }, "mode1": { - "ch1": "mode1_ch1", + "ch1": 1.1, }, "mode2": { - "ch2": "mode2_ch2", + "ch2": 2.2, } } @pytest.mark.parametrize( ("wishlist", "expected"), [ - (None, {"ch1": "nominal_ch1", "ch2": "nominal_ch2"}), - ("nominal", {"ch1": "nominal_ch1", "ch2": "nominal_ch2"}), + ( + None, + { + "ch1": {"coefs": 1.0, "mode": "nominal"}, + "ch2": {"coefs": 2.0, "mode": "nominal"} + } + ), + ( + "nominal", + { + "ch1": {"coefs": 1.0, "mode": "nominal"}, + "ch2": {"coefs": 2.0, "mode": "nominal"} + } + ), ( {("ch1", "ch2"): "nominal"}, - {"ch1": "nominal_ch1", "ch2": "nominal_ch2"} + { + "ch1": {"coefs": 1.0, "mode": "nominal"}, + "ch2": {"coefs": 2.0, "mode": "nominal"} + } ), ( {"ch1": "mode1"}, - {"ch1": "mode1_ch1", "ch2": "nominal_ch2"} + { + "ch1": {"coefs": 1.1, "mode": "mode1"}, + "ch2": {"coefs": 2.0, "mode": "nominal"} + } ), ( {"ch1": "mode1", "ch2": "mode2"}, - {"ch1": "mode1_ch1", "ch2": "mode2_ch2"} + { + "ch1": {"coefs": 1.1, "mode": "mode1"}, + "ch2": {"coefs": 2.2, "mode": "mode2"} + } ), ( {"ch1": "mode1", "ch2": {"gain": 1}}, - {"ch1": "mode1_ch1", "ch2": {"gain": 1}} + { + "ch1": {"coefs": 1.1, "mode": "mode1"}, + "ch2": {"coefs": {"gain": 1}, "mode": "external"} + } ), ] ) @@ -589,7 +613,8 @@ def test_fallback_to_nominal(self, coefs, wishlist, caplog): """Test falling back to nominal coefficients.""" picker = hf.CalibrationCoefficientPicker(coefs, wishlist, fallback="nominal") - assert picker.get_coefs("ch2") == "nominal_ch2" + expected = {"coefs": 2.0, "mode": "nominal"} + assert picker.get_coefs("ch2") == expected assert "Falling back" in caplog.text def test_no_default_coefs(self): From 417c76894887dc251573d5cacff1d93578263aec Mon Sep 17 00:00:00 2001 From: Stephan Finkensieper Date: Fri, 14 Jun 2024 08:44:08 +0000 Subject: [PATCH 13/13] Update usage example --- satpy/readers/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/satpy/readers/utils.py b/satpy/readers/utils.py index b127de87ee..170cc5abcc 100644 --- a/satpy/readers/utils.py +++ b/satpy/readers/utils.py @@ -582,8 +582,6 @@ class CalibrationCoefficientPicker: .. code-block:: python - from satpy.readers.utils import CalibrationCoefficientPicker - coefs = { "nominal": { "ch1": 1.0, @@ -605,6 +603,7 @@ class CalibrationCoefficientPicker: .. code-block:: python + >>> from satpy.readers.utils import CalibrationCoefficientPicker >>> picker = CalibrationCoefficientPicker(coefs, calib_wishlist) >>> picker.get_coefs("ch1") {"coefs": 1.0, "mode": "meirink"} @@ -623,6 +622,7 @@ class CalibrationCoefficientPicker: >>> picker = CalibrationCoefficientPicker(coefs, calib_wishlist, fallback="nominal") >>> picker.get_coefs("ch3") + WARNING No gsics calibration coefficients for ch3. Falling back to nominal. {"coefs": 3.0, "mode": "nominal"} """