Skip to main content

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