Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Frequency range #1912

Merged
merged 1 commit into from Dec 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
101 changes: 100 additions & 1 deletion satpy/dataset/dataid.py
Expand Up @@ -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

Expand Down Expand Up @@ -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


Expand Down
6 changes: 5 additions & 1 deletion satpy/readers/yaml_reader.py
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down
52 changes: 52 additions & 0 deletions satpy/tests/test_yaml_reader.py
Expand Up @@ -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."""

Expand Down