Skip to main content

nyx_space/od/msr/
measurement.rs

1/*
2    Nyx, blazing fast astrodynamics
3    Copyright (C) 2018-onwards Christopher Rabotin <christopher.rabotin@gmail.com>
4
5    This program is free software: you can redistribute it and/or modify
6    it under the terms of the GNU Affero General Public License as published
7    by the Free Software Foundation, either version 3 of the License, or
8    (at your option) any later version.
9
10    This program is distributed in the hope that it will be useful,
11    but WITHOUT ANY WARRANTY; without even the implied warranty of
12    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13    GNU Affero General Public License for more details.
14
15    You should have received a copy of the GNU Affero General Public License
16    along with this program.  If not, see <https://www.gnu.org/licenses/>.
17*/
18
19use crate::od::ground_station::DopplerConfig;
20
21use super::MeasurementType;
22use hifitime::Epoch;
23use indexmap::{IndexMap, IndexSet};
24use log::debug;
25use nalgebra::{DefaultAllocator, DimName, OVector, allocator::Allocator};
26use std::fmt;
27
28#[cfg(feature = "python")]
29use pyo3::prelude::*;
30
31/// A type-agnostic simultaneous measurement storage structure. Allows storing any number of simultaneous measurement of a given taker.
32///
33/// Note that two measurements are considered equal if the tracker and epoch match exactly, and if both have the same measurement types,
34/// and those measurements are equal to within 1e-10 (this allows for some leeway in TDM producers).
35///
36/// :type tracker: str
37/// :type epoch: Epoch
38#[cfg_attr(
39    feature = "python",
40    pyclass(from_py_object),
41    pyo3(module = "nyx_space.od")
42)]
43#[derive(Clone, Debug)]
44pub struct Measurement {
45    /// Tracker alias which made this measurement
46    pub tracker: String,
47    /// Epoch of the measurement
48    pub epoch: Epoch,
49    /// All measurements made simultaneously
50    pub data: IndexMap<MeasurementType, f64>,
51    /// Whether this measurement has been manually rejected
52    pub rejected: bool,
53    /// Doppler measurement should specify a Doppler configuration, unused for other measurement types.
54    pub doppler_config: Option<DopplerConfig>,
55}
56
57#[cfg_attr(feature = "python", pymethods)]
58impl Measurement {
59    /// Correct the provided measurement type with the provided correction, if that measurement type is available
60    ///
61    /// :type msr_type: MeasurementType
62    /// :type correction: float
63    /// :rtype: None
64    pub fn correct(&mut self, msr_type: MeasurementType, correction: f64) {
65        if let Some(cur_value) = self.data.get_mut(&msr_type) {
66            let new_value = *cur_value + correction;
67            debug!("corrected {msr_type:?} from {cur_value} to {new_value}");
68            *cur_value = new_value;
69        }
70    }
71
72    /// Push a measurement type and value.
73    ///
74    /// :type msr_type: MeasurementType
75    /// :type msr_value: float
76    /// :rtype: None
77    pub fn push(&mut self, msr_type: MeasurementType, msr_value: f64) {
78        self.data.insert(msr_type, msr_value);
79    }
80}
81
82impl Measurement {
83    pub fn new(tracker: String, epoch: Epoch) -> Self {
84        Self {
85            tracker,
86            epoch,
87            data: IndexMap::new(),
88            rejected: false,
89            doppler_config: None,
90        }
91    }
92
93    /// Doppler configuration is expected for Doppler measurements, and set to default if not provided.
94    pub fn with(
95        mut self,
96        msr_type: MeasurementType,
97        msr_value: f64,
98        mut doppler_config: Option<DopplerConfig>,
99    ) -> Self {
100        self.push(msr_type, msr_value);
101        if msr_type == MeasurementType::Doppler && doppler_config.is_none() {
102            doppler_config = Some(DopplerConfig::default());
103        }
104        self.doppler_config = doppler_config;
105        self
106    }
107
108    pub fn with_doppler_config(mut self, doppler_config: Option<DopplerConfig>) -> Self {
109        self.doppler_config = doppler_config;
110        self
111    }
112
113    /// Builds an observation vector for this measurement provided a set of measurement types.
114    /// If the requested measurement type is not available, then that specific row is set to zero.
115    /// The caller must set the appropriate sensitivity matrix rows to zero.
116    pub fn observation<S: DimName>(&self, types: &IndexSet<MeasurementType>) -> OVector<f64, S>
117    where
118        DefaultAllocator: Allocator<S>,
119    {
120        // Consider adding a modulo modifier here, any bias should be configured by each ground station.
121        let mut obs = OVector::zeros();
122        for (i, t) in types.iter().enumerate() {
123            if let Some(msr_value) = self.data.get(t) {
124                obs[i] = *msr_value;
125            }
126        }
127        obs
128    }
129
130    /// Returns a vector specifying which measurement types are available.
131    pub fn availability(&self, types: &IndexSet<MeasurementType>) -> Vec<bool> {
132        let mut rtn = vec![false; types.len()];
133        for (i, t) in types.iter().enumerate() {
134            if self.data.contains_key(t) {
135                rtn[i] = true;
136            }
137        }
138        rtn
139    }
140}
141
142impl fmt::Display for Measurement {
143    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144        let msrs = self
145            .data
146            .iter()
147            .map(|(msr_type, msr_value)| format!("{msr_type:?} = {msr_value} {}", msr_type.unit()))
148            .collect::<Vec<String>>()
149            .join(", ");
150
151        write!(f, "{} measured {} on {}", self.tracker, msrs, self.epoch)
152    }
153}
154
155impl PartialEq for Measurement {
156    fn eq(&self, other: &Self) -> bool {
157        self.tracker == other.tracker
158            && self.epoch == other.epoch
159            && self.data.iter().all(|(key, &value)| {
160                if let Some(&other_value) = other.data.get(key) {
161                    (value - other_value).abs() < 1e-10
162                } else {
163                    false
164                }
165            })
166    }
167}