nyx_space/od/ground_station/
mod.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::{Aberration, AzElRange, PhysicsResult};
20use anise::constants::frames::EARTH_J2000;
21use anise::errors::AlmanacResult;
22use anise::prelude::{Almanac, Frame, Orbit};
23use indexmap::{IndexMap, IndexSet};
24use snafu::ensure;
25
26use super::msr::MeasurementType;
27use super::noise::{GaussMarkov, StochasticNoise};
28use super::{ODAlmanacSnafu, ODError, ODTrajSnafu, TrackingDevice};
29use crate::io::ConfigRepr;
30use crate::od::NoiseNotConfiguredSnafu;
31use crate::time::Epoch;
32use hifitime::Duration;
33use rand_pcg::Pcg64Mcg;
34use serde_derive::{Deserialize, Serialize};
35use std::fmt;
36
37pub mod builtin;
38pub mod event;
39pub mod trk_device;
40
41/// GroundStation defines a two-way ranging and doppler station.
42#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
43pub struct GroundStation {
44    pub name: String,
45    /// in degrees
46    pub elevation_mask_deg: f64,
47    /// in degrees
48    pub latitude_deg: f64,
49    /// in degrees
50    pub longitude_deg: f64,
51    /// in km
52    pub height_km: f64,
53    pub frame: Frame,
54    pub measurement_types: IndexSet<MeasurementType>,
55    /// Duration needed to generate a measurement (if unset, it is assumed to be instantaneous)
56    pub integration_time: Option<Duration>,
57    /// Whether to correct for light travel time
58    pub light_time_correction: bool,
59    /// Noise on the timestamp of the measurement
60    pub timestamp_noise_s: Option<StochasticNoise>,
61    pub stochastic_noises: Option<IndexMap<MeasurementType, StochasticNoise>>,
62}
63
64impl GroundStation {
65    /// Initializes a point on the surface of a celestial object.
66    /// This is meant for analysis, not for spacecraft navigation.
67    pub fn from_point(
68        name: String,
69        latitude_deg: f64,
70        longitude_deg: f64,
71        height_km: f64,
72        frame: Frame,
73    ) -> Self {
74        Self {
75            name,
76            elevation_mask_deg: 0.0,
77            latitude_deg,
78            longitude_deg,
79            height_km,
80            frame,
81            measurement_types: IndexSet::new(),
82            integration_time: None,
83            light_time_correction: false,
84            timestamp_noise_s: None,
85            stochastic_noises: None,
86        }
87    }
88
89    /// Returns a copy of this ground station with the new measurement type added (or replaced)
90    pub fn with_msr_type(mut self, msr_type: MeasurementType, noise: StochasticNoise) -> Self {
91        if self.stochastic_noises.is_none() {
92            self.stochastic_noises = Some(IndexMap::new());
93        }
94
95        self.stochastic_noises
96            .as_mut()
97            .unwrap()
98            .insert(msr_type, noise);
99
100        self.measurement_types.insert(msr_type);
101
102        self
103    }
104
105    /// Returns a copy of this ground station without the provided measurement type (if defined, else no error)
106    pub fn without_msr_type(mut self, msr_type: MeasurementType) -> Self {
107        if let Some(noises) = self.stochastic_noises.as_mut() {
108            noises.swap_remove(&msr_type);
109        }
110
111        self.measurement_types.swap_remove(&msr_type);
112
113        self
114    }
115
116    pub fn with_integration_time(mut self, integration_time: Option<Duration>) -> Self {
117        self.integration_time = integration_time;
118
119        self
120    }
121
122    /// Returns a copy of this ground station with the measurement type noises' constant bias set to the provided value.
123    pub fn with_msr_bias_constant(
124        mut self,
125        msr_type: MeasurementType,
126        bias_constant: f64,
127    ) -> Result<Self, ODError> {
128        if self.stochastic_noises.is_none() {
129            self.stochastic_noises = Some(IndexMap::new());
130        }
131
132        let stochastics = self.stochastic_noises.as_mut().unwrap();
133
134        let this_noise = stochastics
135            .get_mut(&msr_type)
136            .ok_or(ODError::NoiseNotConfigured {
137                kind: format!("{msr_type:?}"),
138            })
139            .unwrap();
140
141        if this_noise.bias.is_none() {
142            this_noise.bias = Some(GaussMarkov::ZERO);
143        }
144
145        this_noise.bias.unwrap().constant = Some(bias_constant);
146
147        Ok(self)
148    }
149
150    /// Computes the azimuth and elevation of the provided object seen from this ground station, both in degrees.
151    /// This is a shortcut to almanac.azimuth_elevation_range_sez.
152    pub fn azimuth_elevation_of(
153        &self,
154        rx: Orbit,
155        obstructing_body: Option<Frame>,
156        almanac: &Almanac,
157    ) -> AlmanacResult<AzElRange> {
158        let ab_corr = if self.light_time_correction {
159            Aberration::LT
160        } else {
161            Aberration::NONE
162        };
163        almanac.azimuth_elevation_range_sez(
164            rx,
165            self.to_orbit(rx.epoch, almanac).unwrap(),
166            obstructing_body,
167            ab_corr,
168        )
169    }
170
171    /// Return this ground station as an orbit in its current frame
172    pub fn to_orbit(&self, epoch: Epoch, almanac: &Almanac) -> PhysicsResult<Orbit> {
173        use anise::constants::usual_planetary_constants::MEAN_EARTH_ANGULAR_VELOCITY_DEG_S;
174        Orbit::try_latlongalt(
175            self.latitude_deg,
176            self.longitude_deg,
177            self.height_km,
178            MEAN_EARTH_ANGULAR_VELOCITY_DEG_S,
179            epoch,
180            almanac.frame_from_uid(self.frame).unwrap(),
181        )
182    }
183
184    /// Returns the noises for all measurement types configured for this ground station at the provided epoch, timestamp noise is the first entry.
185    fn noises(&mut self, epoch: Epoch, rng: Option<&mut Pcg64Mcg>) -> Result<Vec<f64>, ODError> {
186        let mut noises = vec![0.0; self.measurement_types.len() + 1];
187
188        if let Some(rng) = rng {
189            ensure!(
190                self.stochastic_noises.is_some(),
191                NoiseNotConfiguredSnafu {
192                    kind: "ground station stochastics".to_string(),
193                }
194            );
195            // Add the timestamp noise first
196
197            if let Some(mut timestamp_noise) = self.timestamp_noise_s {
198                noises[0] = timestamp_noise.sample(epoch, rng);
199            }
200
201            let stochastics = self.stochastic_noises.as_mut().unwrap();
202
203            for (ii, msr_type) in self.measurement_types.iter().enumerate() {
204                noises[ii + 1] = stochastics
205                    .get_mut(msr_type)
206                    .ok_or(ODError::NoiseNotConfigured {
207                        kind: format!("{msr_type:?}"),
208                    })?
209                    .sample(epoch, rng);
210            }
211        }
212
213        Ok(noises)
214    }
215}
216
217impl Default for GroundStation {
218    fn default() -> Self {
219        let mut measurement_types = IndexSet::new();
220        measurement_types.insert(MeasurementType::Range);
221        measurement_types.insert(MeasurementType::Doppler);
222        Self {
223            name: "UNDEFINED".to_string(),
224            measurement_types,
225            elevation_mask_deg: 0.0,
226            latitude_deg: 0.0,
227            longitude_deg: 0.0,
228            height_km: 0.0,
229            frame: EARTH_J2000,
230            integration_time: None,
231            light_time_correction: false,
232            timestamp_noise_s: None,
233            stochastic_noises: None,
234        }
235    }
236}
237
238impl ConfigRepr for GroundStation {}
239
240impl fmt::Display for GroundStation {
241    // Prints the Keplerian orbital elements with units
242    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
243        write!(
244            f,
245            "{} (lat.: {:.4} deg    long.: {:.4} deg    alt.: {:.3} m) [{}]",
246            self.name,
247            self.latitude_deg,
248            self.longitude_deg,
249            self.height_km * 1e3,
250            self.frame,
251        )
252    }
253}
254
255#[cfg(test)]
256mod gs_ut {
257
258    use anise::constants::frames::IAU_EARTH_FRAME;
259    use indexmap::{IndexMap, IndexSet};
260
261    use crate::io::ConfigRepr;
262    use crate::od::prelude::*;
263
264    #[test]
265    fn test_load_single() {
266        use std::env;
267        use std::path::PathBuf;
268
269        use hifitime::TimeUnits;
270
271        let test_data: PathBuf = [
272            env::var("CARGO_MANIFEST_DIR").unwrap(),
273            "data".to_string(),
274            "tests".to_string(),
275            "config".to_string(),
276            "one_ground_station.yaml".to_string(),
277        ]
278        .iter()
279        .collect();
280
281        assert!(test_data.exists(), "Could not find the test data");
282
283        let gs = GroundStation::load(test_data).unwrap();
284
285        dbg!(&gs);
286
287        let mut measurement_types = IndexSet::new();
288        measurement_types.insert(MeasurementType::Range);
289        measurement_types.insert(MeasurementType::Doppler);
290
291        let mut stochastics = IndexMap::new();
292        stochastics.insert(
293            MeasurementType::Range,
294            StochasticNoise {
295                bias: Some(GaussMarkov::new(1.days(), 5e-3).unwrap()),
296                ..Default::default()
297            },
298        );
299        stochastics.insert(
300            MeasurementType::Doppler,
301            StochasticNoise {
302                bias: Some(GaussMarkov::new(1.days(), 5e-5).unwrap()),
303                ..Default::default()
304            },
305        );
306
307        let expected_gs = GroundStation {
308            name: "Demo ground station".to_string(),
309            frame: IAU_EARTH_FRAME,
310            measurement_types,
311            elevation_mask_deg: 5.0,
312            stochastic_noises: Some(stochastics),
313            latitude_deg: 2.3522,
314            longitude_deg: 48.8566,
315            height_km: 0.4,
316            light_time_correction: false,
317            timestamp_noise_s: None,
318            integration_time: Some(60 * Unit::Second),
319        };
320
321        println!("{}", serde_yml::to_string(&expected_gs).unwrap());
322
323        assert_eq!(expected_gs, gs);
324    }
325
326    #[test]
327    fn test_load_many() {
328        use hifitime::TimeUnits;
329        use std::env;
330        use std::path::PathBuf;
331
332        let test_file: PathBuf = [
333            env::var("CARGO_MANIFEST_DIR").unwrap(),
334            "data".to_string(),
335            "tests".to_string(),
336            "config".to_string(),
337            "many_ground_stations.yaml".to_string(),
338        ]
339        .iter()
340        .collect();
341
342        let stations = GroundStation::load_many(test_file).unwrap();
343
344        dbg!(&stations);
345
346        let mut measurement_types = IndexSet::new();
347        measurement_types.insert(MeasurementType::Range);
348        measurement_types.insert(MeasurementType::Doppler);
349
350        let mut stochastics = IndexMap::new();
351        stochastics.insert(
352            MeasurementType::Range,
353            StochasticNoise {
354                bias: Some(GaussMarkov::new(1.days(), 5e-3).unwrap()),
355                ..Default::default()
356            },
357        );
358        stochastics.insert(
359            MeasurementType::Doppler,
360            StochasticNoise {
361                bias: Some(GaussMarkov::new(1.days(), 5e-5).unwrap()),
362                ..Default::default()
363            },
364        );
365
366        let expected = vec![
367            GroundStation {
368                name: "Demo ground station".to_string(),
369                frame: IAU_EARTH_FRAME.with_mu_km3_s2(398600.435436096),
370                measurement_types: measurement_types.clone(),
371                elevation_mask_deg: 5.0,
372                stochastic_noises: Some(stochastics.clone()),
373                latitude_deg: 2.3522,
374                longitude_deg: 48.8566,
375                height_km: 0.4,
376                light_time_correction: false,
377                timestamp_noise_s: None,
378                integration_time: None,
379            },
380            GroundStation {
381                name: "Canberra".to_string(),
382                frame: IAU_EARTH_FRAME.with_mu_km3_s2(398600.435436096),
383                measurement_types,
384                elevation_mask_deg: 5.0,
385                stochastic_noises: Some(stochastics),
386                latitude_deg: -35.398333,
387                longitude_deg: 148.981944,
388                height_km: 0.691750,
389                light_time_correction: false,
390                timestamp_noise_s: None,
391                integration_time: None,
392            },
393        ];
394
395        assert_eq!(expected, stations);
396
397        // Serialize back
398        let reser = serde_yml::to_string(&expected).unwrap();
399        dbg!(reser);
400    }
401}