diff --git a/satpy/dataset/dataid.py b/satpy/dataset/dataid.py index 7093e949b8..9a33bc9e7c 100644 --- a/satpy/dataset/dataid.py +++ b/satpy/dataset/dataid.py @@ -24,7 +24,7 @@ from contextlib import suppress from copy import copy, deepcopy from enum import Enum, IntEnum -from typing import NoReturn +from typing import NamedTuple, NoReturn import numpy as np @@ -72,6 +72,105 @@ def __repr__(self): return '<' + str(self) + '>' +class FrequencyRangeBase(NamedTuple): + """Base class for frequency ranges. + + This is needed because of this bug: https://bugs.python.org/issue41629 + """ + + central: float + bandwidth: float + unit: str = "GHz" + + +class FrequencyRange(FrequencyRangeBase): + """A named tuple for frequency ranges. + + The elements of the range are central and bandwidth values, and optionally + a unit (defaults to GHz). No clever unit conversion is done here, it's just + used for checking that two ranges are comparable. + + This type is used for passive microwave sensors. + + """ + + def __eq__(self, other): + """Return if two channel frequencies are equal. + + Args: + other (tuple or scalar): (central frq, band width frq) or scalar frq + + Return: + True if other is a scalar and min <= other <= max, or if other is + a tuple equal to self, False otherwise. + + """ + if other is None: + return False + elif isinstance(other, numbers.Number): + return other in self + elif isinstance(other, (tuple, list)) and len(other) == 2: + return self[:2] == other + return super().__eq__(other) + + def __ne__(self, other): + """Return the opposite of `__eq__`.""" + return not self == other + + def __lt__(self, other): + """Compare to another frequency.""" + if other is None: + return False + return super().__lt__(other) + + def __gt__(self, other): + """Compare to another frequency.""" + if other is None: + return True + return super().__gt__(other) + + def __hash__(self): + """Hash this tuple.""" + return tuple.__hash__(self) + + def __str__(self): + """Format for print out.""" + return "{0.central} {0.unit} ({0.bandwidth} {0.unit})".format(self) + + def __contains__(self, other): + """Check if this range contains *other*.""" + if other is None: + return False + elif isinstance(other, numbers.Number): + return self.central - self.bandwidth/2. <= other <= self.central + self.bandwidth/2. + + with suppress(AttributeError): + if self.unit != other.unit: + raise NotImplementedError("Can't compare frequency ranges with different units.") + return (self.central - self.bandwidth/2. <= other.central - other.bandwidth/2. and + self.central + self.bandwidth/2. >= other.central + other.bandwidth/2.) + return False + + def distance(self, value): + """Get the distance from value.""" + if self == value: + try: + return abs(value.central - self.central) + except AttributeError: + if isinstance(value, (tuple, list)): + return abs(value[0] - self.central) + return abs(value - self.central) + else: + return np.inf + + @classmethod + def convert(cls, frq): + """Convert `frq` to this type if possible.""" + if isinstance(frq, dict): + return cls(**frq) + return frq + + wlklass = namedtuple("WavelengthRange", "min central max unit", defaults=('µm',)) # type: ignore diff --git a/satpy/readers/yaml_reader.py b/satpy/readers/yaml_reader.py index 3266c32131..1ef600f264 100644 --- a/satpy/readers/yaml_reader.py +++ b/satpy/readers/yaml_reader.py @@ -24,6 +24,7 @@ import warnings from abc import ABCMeta, abstractmethod from collections import OrderedDict, deque +from contextlib import suppress from fnmatch import fnmatch from weakref import WeakValueDictionary @@ -301,7 +302,10 @@ def load_ds_ids_from_config(self): ds_info = dataset.copy() for key in dsid.keys(): if isinstance(ds_info.get(key), dict): - ds_info.update(ds_info[key][dsid.get(key)]) + with suppress(KeyError): + # KeyError is suppressed in case the key does not represent interesting metadata, + # eg a custom type + ds_info.update(ds_info[key][dsid.get(key)]) # this is important for wavelength which was converted # to a tuple ds_info[key] = dsid.get(key) diff --git a/satpy/tests/test_yaml_reader.py b/satpy/tests/test_yaml_reader.py index e594257fcc..d8809ea3ce 100644 --- a/satpy/tests/test_yaml_reader.py +++ b/satpy/tests/test_yaml_reader.py @@ -210,6 +210,58 @@ def test_create_filehandlers(self): self.assertEqual(len(self.reader.file_handlers['ftype1']), 3) +class TestFileYAMLReaderWithCustomIDKey(unittest.TestCase): + """Test units from FileYAMLReader with custo id_keys.""" + + def setUp(self): + """Set up the test case.""" + from satpy.dataset.dataid import FrequencyRange, ModifierTuple + res_dict = {'reader': {'name': 'mhs_l1c_aapp', + 'description': 'AAPP l1c Reader for AMSU-B/MHS data', + 'sensors': ['mhs'], + 'data_identification_keys': {'name': {'required': 'true'}, + 'frequency_range': {'type': FrequencyRange}, + 'resolution': None, + 'polarization': {'enum': ['H', 'V']}, + 'calibration': {'enum': ['brightness_temperature'], + 'transitive': 'true'}, + 'modifiers': {'required': 'true', 'default': [], + 'type': ModifierTuple}}}, + 'datasets': {'1': {'name': '1', + 'frequency_range': {'central': 89., 'bandwidth': 2.8, 'unit': 'GHz'}, + 'polarization': 'V', + 'resolution': 16000, + 'calibration': { + 'brightness_temperature': {'standard_name': 'toa_brightness_temperature'}}, + 'coordinates': ['longitude', 'latitude'], + 'file_type': 'mhs_aapp_l1c'}, + 'latitude': {'name': 'latitude', + 'resolution': 16000, + 'file_type': 'mhs_aapp_l1c', + 'standard_name': 'latitude', + 'units': 'degrees_north'}, + 'longitude': {'name': 'longitude', + 'resolution': 16000, + 'file_type': 'mhs_aapp_l1c', + 'standard_name': 'longitude', + 'units': 'degrees_east'}}, + 'file_types': {'mhs_aapp_l1c': {'file_reader': BaseFileHandler, + 'file_patterns': [ + 'mhsl1c_{platform_shortname}_{start_time:%Y%m%d_%H%M}_{orbit_number:05d}.l1c']}}} # noqa + self.config = res_dict + self.reader = yr.FileYAMLReader(res_dict, + filter_parameters={ + 'start_time': datetime(2000, 1, 1), + 'end_time': datetime(2000, 1, 2), + }) + + def test_custom_type_with_dict_contents_gets_parsed_correctly(self): + """Test custom type with dictionary contents gets parsed correctly.""" + from satpy.dataset.dataid import FrequencyRange + ds_ids = list(self.reader.all_dataset_ids) + assert ds_ids[0]["frequency_range"] == FrequencyRange(89., 2.8, "GHz") + + class TestFileFileYAMLReader(unittest.TestCase): """Test units from FileYAMLReader."""