Source code for madlib._observation

# Copyright (c) 2024 Massachusetts Institute of Technology
# SPDX-License-Identifier: MIT

from dataclasses import dataclass
from typing import List, Self

import numpy as np
from numpy.typing import NDArray

from ._utils import MadlibException


[docs] @dataclass(kw_only=True) class Observation: """ Class for holding observables. All angles are in degrees. Parameters ---------- mjd : float Timestamp of the observation, described as a MJD in UTC ra : float | None Topocentric right ascension angle, by default None dec : float | None Topocentric declination angle, by default None az : float | None Azimuth angle, by default None el : float | None Elevation angle, by default None range_ : float | None Distance between sensor and target, by default None range_rate : float | None Time rate of change of the distance between the sensor and target, by default None lat : float | None Geodetic latitude, by default None lon : float | None Geodetic longitude, by default None sun_el : float | None Elevation angle of sun, by default None sun_separation : float | None Separation angle between target and sun, by default None sensor_id : str | None Unique Sensor ID, by default None """ # time (MJD, UTC) mjd: float # standard observables for optics ra: float | None = None dec: float | None = None # standard observables for radar az: float | None = None el: float | None = None range_: float | None = None range_rate: float | None = None # non-standard observables lat: float | None = None lon: float | None = None # Position of sun sun_el: float | None = None sun_separation: float | None = None # Sensor ID for bookkeeping sensor_id: str | None = None _keys = [ "ra", "dec", "az", "el", "range_", "range_rate", "lat", "lon", "sun_el", "sun_separation", ] def __sub__(self, other: Self) -> "ObservationResidual": """Subtracts another instance of Observation class. Parameters ---------- other : Self The other instance of Observation class to subract Returns ------- Self The subtracted result Raises ------ MadlibException Can only subtract two Observation objects MadlibException Observations must be at the same time for computing a residual """ if not isinstance(other, Observation): raise MadlibException("Can only subtract two Observation objects") d1 = self.__dict__ d2 = other.__dict__ diff = { key: d1[key] - d2[key] for key in self._keys if d1[key] is not None and d2[key] is not None } # special handling of angles that wrap at 0/360 for key in ("ra", "az", "lon"): if d1[key] is not None and d2[key] is not None: temp = np.unwrap(np.array([d1[key], d2[key]]), period=360) diff[key] = temp[0] - temp[1] # add the timestamp to the dict if abs(d1["mjd"] - d2["mjd"]) > 1e-9: raise MadlibException( "Observations must be at the same time for computing a residual" ) diff["mjd"] = d1["mjd"] # Remove the solar elevation _ = diff.pop("sun_el", None) return ObservationResidual(**diff)
[docs] def asarray(self) -> NDArray[np.float64]: """Convert this observation to a flat 1-D array""" return np.array( [val if val is not None else np.NaN for val in self.__dict__.values()] )
[docs] @dataclass(kw_only=True) class ObservationResidual: """ Class for holding the difference between two observables. Parameters ---------- mjd : float Timestamp of the observation, described as a MJD in UTC ra : float | None Topocentric right ascension angle difference, by default None dec : float | None Topocentric declination angle difference, by default None az : float | None Azimuth angle difference, by default None el : float | None Elevation angle difference, by default None range_ : float | None Distance between sensor and target difference, by default None range_rate : float | None Time rate of change of the distance between the sensor and target difference, by default None lat : float | None Geodetic latitude difference, by default None lon : float | None Geodetic longitude difference, by default None """ # time (MJD, UTC) mjd: float # standard observables for optics ra: float | None = None dec: float | None = None # standard observables for radar az: float | None = None el: float | None = None range_: float | None = None range_rate: float | None = None # non-standard observables lat: float | None = None lon: float | None = None
[docs] def asarray(self) -> NDArray[np.float64]: """Convert this observation to a flat 1-D array""" return np.array( [val if val is not None else np.NaN for val in self.__dict__.values()] )
[docs] @dataclass(kw_only=True) class ObservationCollection: """Class for holding observed and true positions of satellites. Parameters ---------- pos_observed: np.ndarray[Observation, np.dtype[np.float64]] Realistic observations of a satellite given sensor noise parameters pos_truth: np.ndarray[Observation, np.dtype[np.float64]] True observations of a satellite ignoring all noise sources pos_expected: np.ndarray[Observation, np.dtype[np.float64]] Observations expected if no noise or maneuvers occur Raises ------ MadlibException Can only add two ObservationCollection objects """ pos_observed: np.ndarray[Observation, np.dtype[np.float64]] pos_truth: np.ndarray[Observation, np.dtype[np.float64]] pos_expected: np.ndarray[Observation, np.dtype[np.float64]] def __add__(self, other: "ObservationCollection"): self.pos_observed = np.concatenate((self.pos_observed, other.pos_observed)) self.pos_truth = np.concatenate((self.pos_truth, other.pos_truth)) self.pos_expected = np.concatenate((self.pos_expected, other.pos_expected))
[docs] def count_valid_observations(self): return len(self.pos_observed)
[docs] def combineObsCollections( collectionList: List[ObservationCollection], ) -> ObservationCollection: """Given observations of a satellite from multiple sensors, combine them into a single object. Parameters ---------- collectionList : List[ObservationCollection] List of observations from multiple sensors of a single satellite. Returns ------- ObservationCollection Combined collection of observations from all sensors. """ pos_observed = np.concatenate([col.pos_observed for col in collectionList]) pos_truth = np.concatenate([col.pos_truth for col in collectionList]) pos_expected = np.concatenate([col.pos_expected for col in collectionList]) combinedCollection = ObservationCollection( pos_observed=pos_observed, pos_truth=pos_truth, pos_expected=pos_expected, ) return combinedCollection