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}