Source code for satpy.readers.pmw_channels_definitions

#!/usr/bin/env python
# -*- coding: utf-8 -*-

# Copyright (c) 2022 Satpy Developers

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""Passive Microwave instrument and channel specific features."""

import numbers
from contextlib import suppress
from typing import NamedTuple

import numpy as np


[docs] class FrequencyBandBaseArithmetics: """Mixin class with basic frequency comparison operations.""" 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)
[docs] @classmethod def convert(cls, frq): """Convert `frq` to this type if possible.""" if isinstance(frq, dict): return cls(**frq) return frq
[docs] class FrequencyQuadrupleSideBandBase(NamedTuple): """Base class for a frequency quadruple side band. Frequency Quadruple Side Band is supposed to describe the special type of bands commonly used in temperature sounding from Passive Microwave Sensors. When the absorption band being observed is symmetrical it is advantageous (giving better NeDT) to sense in a band both right and left of the central absorption frequency. But to avoid (CO2) absorption lines symmetrically positioned on each side of the main absorption band it is common to split the side bands in two 'side-side' bands. This is needed because of this bug: https://bugs.python.org/issue41629 """ central: float side: float sideside: float bandwidth: float unit: str = "GHz"
[docs] class FrequencyQuadrupleSideBand(FrequencyBandBaseArithmetics, FrequencyQuadrupleSideBandBase): """The frequency quadruple side band class. The elements of the quadruple-side-band type frequency band are the central frquency, the relative (main) side band frequency (relative to the center - left and right), the sub-side band frequency (relative to the offset side-band(s)) and their bandwidths. Optionally a unit (defaults to GHz) may be specified. No clever unit conversion is done here, it's just used for checking that two ranges are comparable. Frequency Quadruple Side Band is supposed to describe the special type of bands commonly used in temperature sounding from Passive Microwave Sensors. When the absorption band being observed is symmetrical it is advantageous (giving better NeDT) to sense in a band both right and left of the central absorption frequency. But to avoid (CO2) absorption lines symmetrically positioned on each side of the main absorption band it is common to split the side bands in two 'side-side' bands. """ def __eq__(self, other): """Return if two channel frequencies are equal. Args: other (tuple or scalar): (central frq, side band frq, side-side band frq, and 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, or if other is a number contained by self. False otherwise. """ if other is None: return False if isinstance(other, numbers.Number): return other in self if isinstance(other, (tuple, list)) and len(other) == 4: return other in self return super().__eq__(other) def __str__(self): """Format for print out.""" return f"central={self.central} {self.unit} ±{self.side} ±{self.sideside} width={self.bandwidth} {self.unit}" def __hash__(self): """Hash this tuple.""" return tuple.__hash__(self) def __contains__(self, other): """Check if this quadruple-side-band 'contains' *other*.""" if other is None: return False # The four centrals: central_left_left = self.central - self.side - self.sideside central_left_right = self.central - self.side + self.sideside central_right_left = self.central + self.side - self.sideside central_right_right = self.central + self.side + self.sideside four_centrals = [central_left_left, central_left_right, central_right_left, central_right_right] if isinstance(other, numbers.Number): for central in four_centrals: if _is_inside_interval(other, central, self.bandwidth): return True return False if isinstance(other, (tuple, list)) and len(other) == 5: raise NotImplementedError("Can't check if one frequency quadruple side band is contained in another.") with suppress(AttributeError): if self.unit != other.unit: raise NotImplementedError("Can't compare frequency ranges with different units.") return False
[docs] def distance(self, value): """Get the distance to the quadruple side band. Determining the distance in frequency space between two quadruple side bands can be quite ambiguous, as such bands are in effect a set of 4 narrow bands, two on each side of the main absorption band, and on each side, one on each side of the secondary absorption lines. To keep it as simple as possible we have until further decided to define the distance between such two bands to infinity if they are determined to be equal. If the frequency entered is a single value, the distance will be the minimum of the distances to the two outermost sides of the quadruple side band. If the frequency entered is a tuple or list and the two quadruple frequency bands are contained in each other (equal) the distance will always be zero. """ left_left = self.central - self.side - self.sideside right_right = self.central + self.side + self.sideside if self == value: try: left_side_dist = abs(value.central - value.side - value.sideside - left_left) right_side_dist = abs(value.central + value.side + value.sideside - right_right) except AttributeError: left_side_dist = abs(value - left_left) right_side_dist = abs(value - right_right) return min(left_side_dist, right_side_dist) else: return np.inf
[docs] class FrequencyDoubleSideBandBase(NamedTuple): """Base class for a frequency double side band. Frequency Double Side Band is supposed to describe the special type of bands commonly used in humidty sounding from Passive Microwave Sensors. When the absorption band being observed is symmetrical it is advantageous (giving better NeDT) to sense in a band both right and left of the central absorption frequency. This is needed because of this bug: https://bugs.python.org/issue41629 """ central: float side: float bandwidth: float unit: str = "GHz"
[docs] class FrequencyDoubleSideBand(FrequencyBandBaseArithmetics, FrequencyDoubleSideBandBase): """The frequency double side band class. The elements of the double-side-band type frequency band are the central frquency, the relative side band frequency (relative to the center - left and right) and their bandwidths, 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. Frequency Double Side Band is supposed to describe the special type of bands commonly used in humidty sounding from Passive Microwave Sensors. When the absorption band being observed is symmetrical it is advantageous (giving better NeDT) to sense in a band both right and left of the central absorption frequency. """ def __eq__(self, other): """Return if two channel frequencies are equal. Args: other (tuple or scalar): (central frq, side band frq and 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, or if other is a number contained by self. False otherwise. """ if other is None: return False if isinstance(other, numbers.Number): return other in self if isinstance(other, (tuple, list)) and len(other) == 3: return other in self return super().__eq__(other) def __str__(self): """Format for print out.""" return f"central={self.central} {self.unit} ±{self.side} width={self.bandwidth} {self.unit}" def __hash__(self): """Hash this tuple.""" return tuple.__hash__(self) def __contains__(self, other): """Check if this double-side-band 'contains' *other*.""" if other is None: return False leftside = self.central - self.side rightside = self.central + self.side if isinstance(other, numbers.Number): if self._check_band_contains_other((leftside, self.bandwidth), (other, 0)): return True return self._check_band_contains_other((rightside, self.bandwidth), (other, 0)) other_leftside, other_rightside, other_bandwidth = 0, 0, 0 if isinstance(other, (tuple, list)) and len(other) == 3: other_leftside = other[0] - other[1] other_rightside = other[0] + other[1] other_bandwidth = other[2] else: with suppress(AttributeError): if self.unit != other.unit: raise NotImplementedError("Can't compare frequency ranges with different units.") other_leftside = other.central - other.side other_rightside = other.central + other.side other_bandwidth = other.bandwidth if self._check_band_contains_other((leftside, self.bandwidth), (other_leftside, other_bandwidth)): return True return self._check_band_contains_other((rightside, self.bandwidth), (other_rightside, other_bandwidth))
[docs] @staticmethod def _check_band_contains_other(band, other_band): """Check that a band contains another band. A band is here defined as a tuple of a central frequency and a bandwidth. """ central1, width1 = band central_other, width_other = other_band if ((central1 - width1/2. <= central_other - width_other/2.) and (central1 + width1/2. >= central_other + width_other/2.)): return True return False
[docs] def distance(self, value): """Get the distance to the double side band. Determining the distance in frequency space between two double side bands can be quite ambiguous, as such bands are in effect a set of 2 narrow bands, one on each side of the absorption line. To keep it as simple as possible we have until further decided to set the distance between such two bands to infitiy if neither of them are contained in the other. If the frequency entered is a single value and this frequency falls inside one of the side bands, the distance will be the minimum of the distances to the two outermost sides of the double side band. However, is such a single frequency value falls outside one of the two side bands, the distance will be set to infitiy. If the frequency entered is a tuple the distance will either be 0 (if one is containde in the other) or infinity. """ if self == value: try: left_side_dist = abs(value.central - value.side - (self.central - self.side)) right_side_dist = abs(value.central + value.side - (self.central + self.side)) except AttributeError: if isinstance(value, (tuple, list)): return abs((value[0] - value[1]) - (self.central - self.side)) left_side_dist = abs(value - (self.central - self.side)) right_side_dist = abs(value - (self.central + self.side)) return min(left_side_dist, right_side_dist) else: return np.inf
[docs] 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"
[docs] class FrequencyRange(FrequencyBandBaseArithmetics, FrequencyRangeBase): """The Frequency range class. 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): """Check wether 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, or if other is a number contained by self. False otherwise. """ if other is None: return False if isinstance(other, numbers.Number): return other in self if isinstance(other, (tuple, list)) and len(other) == 2: return self[:2] == other return super().__eq__(other) def __str__(self): """Format for print out.""" return f"central={self.central} {self.unit} width={self.bandwidth} {self.unit}" def __hash__(self): """Hash this tuple.""" return tuple.__hash__(self) def __contains__(self, other): """Check if this range contains *other*.""" if other is None: return False if 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
[docs] 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
[docs] def _is_inside_interval(value, central, width): return central - width/2 <= value <= central + width/2