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 super::MeasurementType;
20use hifitime::Epoch;
21use indexmap::{IndexMap, IndexSet};
22use log::debug;
23use nalgebra::{DefaultAllocator, DimName, OVector, allocator::Allocator};
24use std::fmt;
25
26#[cfg(feature = "python")]
27use pyo3::prelude::*;
28
29/// A type-agnostic simultaneous measurement storage structure. Allows storing any number of simultaneous measurement of a given taker.
30///
31/// Note that two measurements are considered equal if the tracker and epoch match exactly, and if both have the same measurement types,
32/// and those measurements are equal to within 1e-10 (this allows for some leeway in TDM producers).
33///
34/// :type tracker: str
35/// :type epoch: Epoch
36#[cfg_attr(
37    feature = "python",
38    pyclass(from_py_object),
39    pyo3(module = "nyx_space.od")
40)]
41#[derive(Clone, Debug)]
42pub struct Measurement {
43    /// Tracker alias which made this measurement
44    pub tracker: String,
45    /// Epoch of the measurement
46    pub epoch: Epoch,
47    /// All measurements made simultaneously
48    pub data: IndexMap<MeasurementType, f64>,
49    /// Whether this measurement has been manually rejected
50    pub rejected: bool,
51}
52
53#[cfg_attr(feature = "python", pymethods)]
54impl Measurement {
55    /// Correct the provided measurement type with the provided correction, if that measurement type is available
56    ///
57    /// :type msr_type: MeasurementType
58    /// :type correction: float
59    /// :rtype: None
60    pub fn correct(&mut self, msr_type: MeasurementType, correction: f64) {
61        if let Some(cur_value) = self.data.get_mut(&msr_type) {
62            let new_value = *cur_value + correction;
63            debug!("corrected {msr_type:?} from {cur_value} to {new_value}");
64            *cur_value = new_value;
65        }
66    }
67
68    /// Push a measurement type and value.
69    ///
70    /// :type msr_type: MeasurementType
71    /// :type msr_value: float
72    /// :rtype: None
73    pub fn push(&mut self, msr_type: MeasurementType, msr_value: f64) {
74        self.data.insert(msr_type, msr_value);
75    }
76}
77
78impl Measurement {
79    pub fn new(tracker: String, epoch: Epoch) -> Self {
80        Self {
81            tracker,
82            epoch,
83            data: IndexMap::new(),
84            rejected: false,
85        }
86    }
87
88    pub fn with(mut self, msr_type: MeasurementType, msr_value: f64) -> Self {
89        self.push(msr_type, msr_value);
90        self
91    }
92
93    /// Builds an observation vector for this measurement provided a set of measurement types.
94    /// If the requested measurement type is not available, then that specific row is set to zero.
95    /// The caller must set the appropriate sensitivity matrix rows to zero.
96    pub fn observation<S: DimName>(&self, types: &IndexSet<MeasurementType>) -> OVector<f64, S>
97    where
98        DefaultAllocator: Allocator<S>,
99    {
100        // Consider adding a modulo modifier here, any bias should be configured by each ground station.
101        let mut obs = OVector::zeros();
102        for (i, t) in types.iter().enumerate() {
103            if let Some(msr_value) = self.data.get(t) {
104                obs[i] = *msr_value;
105            }
106        }
107        obs
108    }
109
110    /// Returns a vector specifying which measurement types are available.
111    pub fn availability(&self, types: &IndexSet<MeasurementType>) -> Vec<bool> {
112        let mut rtn = vec![false; types.len()];
113        for (i, t) in types.iter().enumerate() {
114            if self.data.contains_key(t) {
115                rtn[i] = true;
116            }
117        }
118        rtn
119    }
120}
121
122impl fmt::Display for Measurement {
123    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124        let msrs = self
125            .data
126            .iter()
127            .map(|(msr_type, msr_value)| format!("{msr_type:?} = {msr_value} {}", msr_type.unit()))
128            .collect::<Vec<String>>()
129            .join(", ");
130
131        write!(f, "{} measured {} on {}", self.tracker, msrs, self.epoch)
132    }
133}
134
135impl PartialEq for Measurement {
136    fn eq(&self, other: &Self) -> bool {
137        self.tracker == other.tracker
138            && self.epoch == other.epoch
139            && self.data.iter().all(|(key, &value)| {
140                if let Some(&other_value) = other.data.get(key) {
141                    (value - other_value).abs() < 1e-10
142                } else {
143                    false
144                }
145            })
146    }
147}