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}