Skip to main content

nyx_space/od/msr/
types.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 anise::astro::AzElRange;
20use arrow::datatypes::{DataType, Field};
21use serde::{Deserialize, Serialize};
22use std::{collections::HashMap, str::FromStr};
23
24use crate::{io::InputOutputError, od::ODError};
25
26#[cfg(feature = "python")]
27use pyo3::prelude::*;
28
29#[cfg_attr(feature = "python", pyclass, pyo3(module = "nyx_space.od"))]
30#[derive(Copy, Clone, Debug, Hash, Serialize, Deserialize, PartialEq, Eq, der::Enumerated)]
31#[repr(u8)]
32pub enum MeasurementType {
33    #[serde(rename = "range_km")]
34    Range = 0,
35    #[serde(rename = "doppler_km_s")]
36    Doppler = 1,
37    #[serde(rename = "azimuth_deg")]
38    Azimuth = 2,
39    #[serde(rename = "elevation_deg")]
40    Elevation = 3,
41    #[serde(rename = "receive_freq")]
42    ReceiveFrequency = 4,
43    #[serde(rename = "transmit_freq")]
44    TransmitFrequency = 5,
45    #[serde(rename = "x")]
46    X = 6,
47    #[serde(rename = "y")]
48    Y = 7,
49    #[serde(rename = "z")]
50    Z = 8,
51}
52
53impl MeasurementType {
54    /// Returns the expected unit of this measurement type
55    pub fn unit(self) -> &'static str {
56        match self {
57            Self::Range => "km",
58            Self::Doppler => "km/s",
59            Self::Azimuth | Self::Elevation => "deg",
60            Self::ReceiveFrequency | Self::TransmitFrequency => "Hz",
61            Self::X | Self::Y | Self::Z => "km",
62        }
63    }
64
65    /// Returns true if this measurement type could be a two-way measurement.
66    pub(crate) fn may_be_two_way(self) -> bool {
67        match self {
68            MeasurementType::Range | MeasurementType::Doppler => true,
69            MeasurementType::Azimuth
70            | MeasurementType::Elevation
71            | MeasurementType::ReceiveFrequency
72            | MeasurementType::TransmitFrequency
73            | MeasurementType::X
74            | MeasurementType::Y
75            | MeasurementType::Z => false,
76        }
77    }
78
79    /// Returns the fields for this kind of measurement. The metadata includes a `unit` field with the unit.
80    /// Column is nullable in case there is no such measurement at a given epoch.
81    pub fn to_field(&self) -> Field {
82        let mut meta = HashMap::new();
83        meta.insert("unit".to_string(), self.unit().to_string());
84
85        Field::new(
86            format!("{self:?} ({})", self.unit()),
87            DataType::Float64,
88            true,
89        )
90        .with_metadata(meta)
91    }
92
93    /// Computes the one way measurement from an AER object and the noise of this measurement type, returned in the units of this measurement type.
94    pub fn compute_one_way(self, aer: AzElRange, noise: f64) -> Result<f64, ODError> {
95        match self {
96            Self::Range => Ok(aer.range_km + noise),
97            Self::Doppler => Ok(aer.range_rate_km_s + noise),
98            Self::Azimuth => Ok(aer.azimuth_deg + noise),
99            Self::Elevation => Ok(aer.elevation_deg + noise),
100            Self::ReceiveFrequency | Self::TransmitFrequency => Err(ODError::MeasurementSimError {
101                details: format!("{self:?} is only supported in CCSDS TDM parsing"),
102            }),
103            Self::X | Self::Y | Self::Z => Err(ODError::MeasurementSimError {
104                details: format!("{self:?} must be computed directly from the state"),
105            }),
106        }
107    }
108
109    /// Computes the two way measurement from two AER values and the noise of this measurement type, returned in the units of this measurement type.
110    /// Two way is modeled by averaging the measurement in between both times, and adding the noise divided by sqrt(2).
111    pub fn compute_two_way(
112        self,
113        aer_t0: AzElRange,
114        aer_t1: AzElRange,
115        noise: f64,
116    ) -> Result<f64, ODError> {
117        match self {
118            Self::Range => {
119                let range_km = (aer_t1.range_km + aer_t0.range_km) * 0.5;
120                Ok(range_km + noise / 2.0_f64.sqrt())
121            }
122            Self::Doppler => {
123                let doppler_km_s = (aer_t1.range_rate_km_s + aer_t0.range_rate_km_s) * 0.5;
124                Ok(doppler_km_s + noise / 2.0_f64.sqrt())
125            }
126            Self::Azimuth => {
127                let az_deg = (aer_t1.azimuth_deg + aer_t0.azimuth_deg) * 0.5;
128                Ok(az_deg + noise / 2.0_f64.sqrt())
129            }
130            Self::Elevation => {
131                let el_deg = (aer_t1.elevation_deg + aer_t0.elevation_deg) * 0.5;
132                Ok(el_deg + noise / 2.0_f64.sqrt())
133            }
134            Self::ReceiveFrequency | Self::TransmitFrequency => Err(ODError::MeasurementSimError {
135                details: format!("{self:?} is only supported in CCSDS TDM parsing"),
136            }),
137            Self::X | Self::Y | Self::Z => Err(ODError::MeasurementSimError {
138                details: format!("{self:?} is not supported for two way measurements"),
139            }),
140        }
141    }
142
143    /// Returns the CCSDS TDM name for this measurement type.
144    pub fn ccsds_tdm_name(&self) -> &str {
145        match self {
146            MeasurementType::Range => "RANGE",
147            MeasurementType::Doppler => "DOPPLER_INTEGRATED",
148            MeasurementType::Azimuth => "ANGLE_1",
149            MeasurementType::Elevation => "ANGLE_2",
150            MeasurementType::ReceiveFrequency => "RECEIVE_FREQ",
151            MeasurementType::TransmitFrequency => "TRANSMIT_FREQ",
152            MeasurementType::X => "X",
153            MeasurementType::Y => "Y",
154            MeasurementType::Z => "Z",
155        }
156    }
157}
158
159impl FromStr for MeasurementType {
160    type Err = InputOutputError;
161
162    fn from_str(s: &str) -> Result<Self, Self::Err> {
163        match s.to_lowercase().as_str() {
164            "range" => Ok(Self::Range),
165            "doppler" => Ok(Self::Doppler),
166            "azimuth" => Ok(Self::Azimuth),
167            "elevation" => Ok(Self::Elevation),
168            "x" => Ok(Self::X),
169            "y" => Ok(Self::Y),
170            "z" => Ok(Self::Z),
171            _ => Err(InputOutputError::UnsupportedData {
172                which: s.to_string(),
173            }),
174        }
175    }
176}