nyx_space/od/noise/
link_specific.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::{StochasticNoise, WhiteNoise};
20use anise::constants::SPEED_OF_LIGHT_KM_S;
21use hifitime::Duration;
22use serde_derive::{Deserialize, Serialize};
23use std::f64::consts::TAU;
24
25/// Signal power to noise density for stochastic modeling, typical values.
26/// IMPORTANT: The S/N0 will always be lower or equal to the Carrier Power to noise density (C/N0)
27#[derive(Copy, Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
28pub enum SN0 {
29    /// 65 dB-Hz
30    Strong,
31    /// 50 dB-Hz
32    #[default]
33    Average,
34    /// 40 dB-Hz
35    Poor,
36    /// Manual value provided in dB-Hz, converted to Hertz automatically
37    ManualDbHz(f64),
38}
39
40impl SN0 {
41    /// Note that this returns the data in Hertz not dB-Hz
42    pub(crate) fn value_hz(self) -> f64 {
43        match self {
44            Self::Strong => 10.0_f64.powf(6.5),
45            Self::Average => 10.0_f64.powi(5),
46            Self::Poor => 10.0_f64.powi(4),
47            Self::ManualDbHz(value) => 10.0_f64.powf(value / 10.0),
48        }
49    }
50}
51
52/// Carrier power to noise density for stochastic modeling, typical values.
53/// IMPORTANT: The C/N0 will always be greater or equal to the Signal Power to noise density (C/N0) because the S/N0 for a ranging
54/// tone is the power dedicated to the ranging within the uplink (cf. "modulation index" in the DESCANSO monograph).
55#[derive(Copy, Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
56pub enum CN0 {
57    /// 70 dB-Hz
58    Strong,
59    /// 55 dB-Hz
60    #[default]
61    Average,
62    /// 45 dB-Hz
63    Poor,
64    /// Manual value provided in dB-Hz, converted to Hertz automatically
65    ManualDbHz(f64),
66}
67
68impl CN0 {
69    /// Note that this returns the data in Hertz not dB-Hz
70    pub(crate) fn value_hz(self) -> f64 {
71        match self {
72            Self::Strong => 10.0_f64.powi(7),
73            Self::Average => 10.0_f64.powf(5.5),
74            Self::Poor => 10.0_f64.powf(4.5),
75            Self::ManualDbHz(value) => 10.0_f64.powf(value / 10.0),
76        }
77    }
78}
79
80/// Carrier frequency helper enum, typical values.
81pub enum CarrierFreq {
82    /// 2.2 GHz
83    SBand,
84    /// 8.4 GHz
85    XBand,
86    /// 32 Ghz
87    KaBand,
88    ManualHz(f64),
89}
90
91impl CarrierFreq {
92    pub(crate) fn value_hz(self) -> f64 {
93        match self {
94            Self::SBand => 2.2e9,
95            Self::XBand => 8.4e9,
96            Self::KaBand => 32e9,
97            Self::ManualHz(value) => value,
98        }
99    }
100}
101
102/// An enum helper with typical chip rates.
103#[derive(Copy, Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
104pub enum ChipRate {
105    /// 1 kchip/s -- basically emergency ranging
106    Lowest,
107    /// 100 kchip/s -- could be used for weaker links
108    Low,
109    /// 1 Mchip/s -- typical of xGEO/cislunar missions
110    #[default]
111    StandardT4B,
112    /// 10 Mchip/s -- high-precision scientific missions (e.g. gravity modeling)
113    High,
114    /// 25 Mchip/s -- highly specialized missions
115    VeryHigh,
116    /// Provide your own chip rate depending on the ground station configuration
117    ManualHz(f64),
118}
119
120impl ChipRate {
121    pub(crate) fn value_chip_s(self) -> f64 {
122        match self {
123            Self::Lowest => 1e3,
124            Self::Low => 1e5,
125            Self::StandardT4B => 1e6,
126            Self::High => 1e7,
127            Self::VeryHigh => 2.5e7,
128            Self::ManualHz(value) => value,
129        }
130    }
131}
132
133impl StochasticNoise {
134    /// Constructs a high precision zero-mean range noise model (accounting for clock error and thermal error) from
135    /// the Allan deviation of the clock, integration time, chip rate (depends on the ranging code), and
136    /// signal-power-to-noise-density ratio (S/N₀).
137    ///
138    /// NOTE: The Allan Deviation should be provided given the integration time. For example, if the integration time
139    /// is one second, the Allan Deviation should be the deviation over one second.
140    ///
141    /// IMPORTANT: These do NOT include atmospheric noises, which add up to ~10 cm one-sigma.
142    pub fn high_prec_range_km(
143        allan_deviation: f64,
144        integration_time: Duration,
145        chip_rate: ChipRate,
146        s_n0: SN0,
147    ) -> Self {
148        // Compute the thermal noise.
149        let sigma_thermal_km =
150            SPEED_OF_LIGHT_KM_S / (TAU * chip_rate.value_chip_s() * (2.0 * s_n0.value_hz()).sqrt());
151        // Compute the clock noise.
152        let sigma_clock_km =
153            (SPEED_OF_LIGHT_KM_S * allan_deviation * integration_time.to_seconds())
154                / (3.0_f64.sqrt());
155
156        Self {
157            white_noise: Some(WhiteNoise::constant_white_noise(
158                (sigma_clock_km.powi(2) + sigma_thermal_km.powi(2)).sqrt(),
159            )),
160            bias: None,
161        }
162    }
163
164    pub fn high_prec_doppler_km_s(
165        allan_deviation: f64,
166        integration_time: Duration,
167        carrier: CarrierFreq,
168        c_n0: CN0,
169    ) -> Self {
170        // Compute the thermal noise
171        let sigma_thermal_km_s = SPEED_OF_LIGHT_KM_S
172            / (TAU
173                * carrier.value_hz()
174                * (2.0 * c_n0.value_hz() * integration_time.to_seconds()).sqrt());
175
176        // Compute the clock noise.
177        let sigma_clock_km_s = SPEED_OF_LIGHT_KM_S * allan_deviation;
178
179        Self {
180            white_noise: Some(WhiteNoise::constant_white_noise(
181                (sigma_clock_km_s.powi(2) + sigma_thermal_km_s.powi(2)).sqrt(),
182            )),
183            bias: None,
184        }
185    }
186}
187
188#[cfg(test)]
189mod link_noise {
190    use super::{CarrierFreq, ChipRate, StochasticNoise, CN0, SN0};
191    use hifitime::Unit;
192    #[test]
193    fn nasa_dsac() {
194        // The DSAC has an Allan Dev of 1e-14 over one day.
195        // Gemini claims that such a good clock likely has the same deviation over 60 seconds (hitting the flicker floor).
196        // But worst case scenario, its AD is the square root of the ratio of one day over 1 minute, or 38 times worse.
197
198        for (case_num, allan_dev) in [1e-14, 3.8e-13].iter().copied().enumerate() {
199            println!("AD = {allan_dev:e}");
200
201            let range_dsac_no_flicker = StochasticNoise::high_prec_range_km(
202                allan_dev,
203                Unit::Minute * 1,
204                ChipRate::StandardT4B,
205                SN0::Average,
206            );
207
208            let range_sigma_m = range_dsac_no_flicker.white_noise.unwrap().sigma * 1e3;
209
210            println!("range sigma = {range_sigma_m:.3e} m",);
211
212            assert!(range_sigma_m.abs() < 1.1e-1);
213
214            let doppler_dsac_no_flicker = StochasticNoise::high_prec_doppler_km_s(
215                allan_dev,
216                Unit::Minute * 1,
217                CarrierFreq::XBand,
218                CN0::Average,
219            );
220
221            let doppler_sigma_m_s = doppler_dsac_no_flicker.white_noise.unwrap().sigma * 1e3;
222
223            println!("doppler sigma = {doppler_sigma_m_s:.3e} m/s");
224
225            match case_num {
226                0 => assert!(doppler_sigma_m_s < 3.2e-6),
227                1 => assert!(doppler_sigma_m_s < 1.2e-4),
228                _ => unreachable!(),
229            };
230        }
231    }
232}