Source code for ccsdspy.converters

"""This class hold the implementation of the converter system, which applies 
post-process to decoded packet fields. This post-processing includes applying
linear/polynomial calibration curves, dictionary replacement, and time parsing.
"""

from datetime import datetime, timedelta

import numpy as np

__all__ = [
    "EnumConverterMissingKey",
    "Converter",
    "PolyConverter",
    "LinearConverter",
    "EnumConverter",
    "DatetimeConverter",
]


[docs]class EnumConverterMissingKey(RuntimeError): """During conversion a value was encountered which did not have a corresponding key in the replacement dictionary. """
[docs]class Converter: """Base class for all converter objects. This class is extended to create converters, and users may extend this class to write their own custom converters. To write a converter, one must create a subclass and override either the method `convert(*field_arrays)`. This method implements the conversion for an entire sequence of decoded packet field values in a single call. """ def __init__(self): raise NotImplementedError("This is a base class not meant to be instantiated directly")
[docs] def convert(self, field_array): """Convert a sequence of decoded packet field values. Parameters ---------- field_array : NumPy array decoded packet field values, must have at least one dimension Returns ------- converted_field_array : NumPy array converted form of the decoded packet field values """ raise NotImplementedError("This method must be overridden by a subclass")
[docs]class PolyConverter(Converter): """Post-processing conversion which applies calibration using a series of coefficients ordered from highest power to intercept. """ def __init__(self, coeffs): """Instantiate a PolyConverter object Parameters ---------- coeffs : list of float Polynomial coefficients ordered from highest power to intercept. """ self._coeffs = coeffs
[docs] def convert(self, field_array): """Apply the polynomial conversion. Parameters ---------- field_array : NumPy array decoded packet field values, must have at least one dimension Returns ------- converted : NumPy array converted form of the decoded packet field values """ converted = np.zeros(field_array.shape, dtype=np.float64) for power, coeff in enumerate(reversed(self._coeffs)): converted += coeff * field_array**power return converted
[docs]class LinearConverter(PolyConverter): """Post-processing conversion which applies a linear (y=mx+b) transformation. """ def __init__(self, slope, intercept): """Instantiate a LinearConverter""" super().__init__([slope, intercept])
[docs]class EnumConverter(Converter): """Post-processing conversion for applying dictionary replacement of integers to strings. If during conversion a value is encountered which does not have a corresponding key in the replacement dictionary, then a `:py:class:`~ccsdspy.converters.EnumConverterMissingKey` exception will be thrown. """ def __init__(self, replace_dict): """Initialize a EnumConverter. Parameters ---------- replace_dict : dict of int to string Replacement dictionary mapping integer values to string values Raises ------ TypeError Either one of the keys of the replacement dictionary is not an integer, or one of the values is not a string. """ self._replace_dict = replace_dict for key, value in replace_dict.items(): if not isinstance(key, int): raise TypeError( f"Found key in EnumConverter replacement dictionary that is " f"not an integer: {repr(key)}" ) if not isinstance(value, str): raise TypeError( f"Found value in EnumConverter replacement dictionary that is " f"not a string: {repr(value)}" )
[docs] def convert(self, field_array): """Apply the enum replacement conversion. Parameters ---------- field_array : NumPy array decoded packet field values, must have at least one dimension Returns ------- converted : NumPy array converted form of the decoded packet field values """ converted = np.zeros(field_array.shape, dtype=object) converted_mask = np.zeros(field_array.shape, dtype=bool) for key, value in self._replace_dict.items(): converted[field_array == key] = value converted_mask[field_array == key] = True if not converted_mask.all(): missing_keys = field_array[~converted_mask].tolist() raise EnumConverterMissingKey( f"The following were encountered which did not have " f"corresponding keys in the replacment dictionary: " f"{repr(missing_keys)}" ) return converted
[docs]class DatetimeConverter(Converter): """Post-processing conversion for converting timestamp fields to datetime instances, computed using offset(s) from a reference time. This class supports the offsets stored in multiple input fields, for example where one field is a coarse time (eg seconds) and a second field is a fine time (eg nanoseconds). To use multiple input fields, pass a tuple of input field names when this converter is added to the packet. """ _VALID_UNITS = ( "days", "hours", "minutes", "seconds", "milliseconds", "microseconds", "nanoseconds", ) _MILLISECONDS_PER_SECOND = 1_000 _MICROSECONDS_PER_SECOND = 1_000_000 _NANOSECONDS_PER_SECOND = 1_000_000_000 def __init__(self, since, units): """Initialize a DatetimeConverter Parameters ---------- since : datetime Reference datetime. The time stored in the field(s) is considered an offset to this reference. If this has timezone information attached to it, so will the converted datetimes. units : str or tuple of str Units string of tuples of units strings for the offset of each input field. Valid units are "days", "minutes", "milliseconds", "microseconds", and "nanoseconds". Raises ------ TypeError One of the input arguments is not of the correct type ValueError One or more of the units are invalid """ if not isinstance(since, datetime): raise TypeError("Argument 'since' must be an instance of datetime") if isinstance(units, str): units_tuple = (units,) elif isinstance(units, tuple): units_tuple = units else: raise TypeError("Argument 'units' must be either a string or tuple") if not (set(units_tuple) <= set(self._VALID_UNITS)): raise ValueError("One or more units are invalid") self._since = since self._units = units_tuple
[docs] def convert(self, *field_arrays): """Apply the datetime conversion. Parameters ---------- field_arrays : list of NumPy array list of decoded packet field values, each must have at least one dimension Returns ------- converted : NumPy array of object (holding datetimes) converted form of the decoded packet field values Raises ------ ValueError Too many or too few units were provided, as compared to the input field arrays sent. """ assert len(field_arrays) > 0, "Must have at least one input field" converted = [] for field_values in zip(*field_arrays): converted_time = self._since for unit, offset_raw in zip(self._units, field_values): offset_raw = float(offset_raw) if unit == "days": converted_time += timedelta(days=offset_raw) elif unit == "hours": converted_time += timedelta(hours=offset_raw) elif unit == "minutes": converted_time += timedelta(minutes=offset_raw) elif unit == "seconds": converted_time += timedelta(seconds=offset_raw) elif unit == "milliseconds": converted_time += timedelta(seconds=offset_raw / self._MILLISECONDS_PER_SECOND) elif unit == "microseconds": converted_time += timedelta(seconds=offset_raw / self._MICROSECONDS_PER_SECOND) elif unit == "nanoseconds": converted_time += timedelta(seconds=offset_raw / self._NANOSECONDS_PER_SECOND) converted.append(converted_time) converted = np.array(converted, dtype=object) return converted