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};
20use anise::errors::{AlmanacError, AlmanacResult};
21use anise::prelude::{Almanac, Frame, Orbit};
22use der::{Decode, Encode, Reader};
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::{Deserialize, Serialize};
35use std::fmt::{self, Debug};
36
37pub mod builtin;
38pub mod trk_device;
39
40#[cfg(feature = "python")]
41use pyo3::exceptions::PyValueError;
42#[cfg(feature = "python")]
43use pyo3::prelude::*;
44#[cfg(feature = "python")]
45use pyo3::types::{PyBytes, PyType};
46#[cfg(feature = "python")]
47mod python;
48
49/// GroundStation defines a one-way or two-way ranging and doppler station. Set the integration time for two-way.
50///
51/// :type name: str
52/// :type location: Location
53/// :type stochastic_noises: dict[MeasurementType, StochasticNoise]
54/// :type integration_time: Duration | None
55/// :type light_time_correction: bool | None
56/// :type timestamp_noise_s: StochasticNoise | None
57#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
58#[cfg_attr(feature = "python", pyclass(from_py_object))]
59pub struct GroundStation {
60    pub name: String,
61    pub location: Location,
62    pub measurement_types: IndexSet<MeasurementType>,
63    /// Duration needed to generate a measurement (if unset, it is assumed to be instantaneous)
64    pub integration_time: Option<Duration>,
65    /// Whether to correct for light travel time
66    pub light_time_correction: bool,
67    /// Noise on the timestamp of the measurement
68    pub timestamp_noise_s: Option<StochasticNoise>,
69    pub stochastic_noises: Option<IndexMap<MeasurementType, StochasticNoise>>,
70}
71
72#[cfg_attr(feature = "python", pymethods)]
73impl GroundStation {
74    /// Computes the azimuth and elevation of the provided object seen from this ground station, both in degrees.
75    /// This is a shortcut to almanac.azimuth_elevation_range_sez.
76    ///
77    /// :type rx: Orbit
78    /// :type obstructing_body: Frame | None
79    /// :type almanac: Almanac
80    /// :rtype: AzElRange
81    pub fn azimuth_elevation_of(
82        &self,
83        rx: Orbit,
84        obstructing_body: Option<Frame>,
85        almanac: &Almanac,
86    ) -> AlmanacResult<AzElRange> {
87        let ab_corr = if self.light_time_correction {
88            Aberration::LT
89        } else {
90            Aberration::NONE
91        };
92        almanac.azimuth_elevation_range_sez(
93            rx,
94            self.to_orbit(rx.epoch, almanac)?,
95            obstructing_body,
96            ab_corr,
97        )
98    }
99
100    /// Return this ground station as an orbit in its current frame
101    ///
102    /// :type epoch: Epoch
103    /// :type almanac: Almanac
104    /// :rtype: Orbit
105    pub fn to_orbit(&self, epoch: Epoch, almanac: &Almanac) -> AlmanacResult<Orbit> {
106        Orbit::try_latlongalt(
107            self.location.latitude_deg,
108            self.location.longitude_deg,
109            self.location.height_km,
110            epoch,
111            almanac.frame_info(self.location.frame).map_err(|source| {
112                AlmanacError::GenericError {
113                    err: source.to_string(),
114                }
115            })?,
116        )
117        .map_err(|source| AlmanacError::AlmanacPhysics {
118            action: "building ground station location",
119            source: Box::new(source),
120        })
121    }
122}
123
124impl GroundStation {
125    /// Initializes a point on the surface of a celestial object.
126    /// This is meant for analysis, not for spacecraft navigation.
127    pub fn from_point(
128        name: String,
129        latitude_deg: f64,
130        longitude_deg: f64,
131        height_km: f64,
132        frame: Frame,
133    ) -> Self {
134        Self {
135            name,
136            location: Location {
137                latitude_deg,
138                longitude_deg,
139                height_km,
140                frame: frame.into(),
141                terrain_mask: vec![],
142                terrain_mask_ignored: true,
143            },
144            measurement_types: IndexSet::new(),
145            integration_time: None,
146            light_time_correction: false,
147            timestamp_noise_s: None,
148            stochastic_noises: None,
149        }
150    }
151
152    /// Returns a copy of this ground station with the new measurement type added (or replaced)
153    pub fn with_msr_type(mut self, msr_type: MeasurementType, noise: StochasticNoise) -> Self {
154        if self.stochastic_noises.is_none() {
155            self.stochastic_noises = Some(IndexMap::new());
156        }
157
158        self.stochastic_noises
159            .as_mut()
160            .unwrap()
161            .insert(msr_type, noise);
162
163        self.measurement_types.insert(msr_type);
164
165        self
166    }
167
168    /// Returns a copy of this ground station without the provided measurement type (if defined, else no error)
169    pub fn without_msr_type(mut self, msr_type: MeasurementType) -> Self {
170        if let Some(noises) = self.stochastic_noises.as_mut() {
171            noises.swap_remove(&msr_type);
172        }
173
174        self.measurement_types.swap_remove(&msr_type);
175
176        self
177    }
178
179    pub fn with_integration_time(mut self, integration_time: Option<Duration>) -> Self {
180        self.integration_time = integration_time;
181
182        self
183    }
184
185    /// Returns a copy of this ground station with the measurement type noises' constant bias set to the provided value.
186    pub fn with_msr_bias_constant(
187        mut self,
188        msr_type: MeasurementType,
189        bias_constant: f64,
190    ) -> Result<Self, ODError> {
191        if self.stochastic_noises.is_none() {
192            self.stochastic_noises = Some(IndexMap::new());
193        }
194
195        let stochastics = self.stochastic_noises.as_mut().unwrap();
196
197        let this_noise = stochastics
198            .get_mut(&msr_type)
199            .ok_or(ODError::NoiseNotConfigured {
200                kind: format!("{msr_type:?}"),
201            })
202            .unwrap();
203
204        if this_noise.bias.is_none() {
205            this_noise.bias = Some(GaussMarkov::ZERO);
206        }
207
208        this_noise.bias.unwrap().constant = Some(bias_constant);
209
210        Ok(self)
211    }
212
213    /// Returns the noises for all measurement types configured for this ground station at the provided epoch, timestamp noise is the first entry.
214    fn noises(&mut self, epoch: Epoch, rng: Option<&mut Pcg64Mcg>) -> Result<Vec<f64>, ODError> {
215        let mut noises = vec![0.0; self.measurement_types.len() + 1];
216
217        if let Some(rng) = rng {
218            ensure!(
219                self.stochastic_noises.is_some(),
220                NoiseNotConfiguredSnafu {
221                    kind: "ground station stochastics".to_string(),
222                }
223            );
224            // Add the timestamp noise first
225
226            if let Some(mut timestamp_noise) = self.timestamp_noise_s {
227                noises[0] = timestamp_noise.sample(epoch, rng);
228            }
229
230            let stochastics = self.stochastic_noises.as_mut().unwrap();
231
232            for (ii, msr_type) in self.measurement_types.iter().enumerate() {
233                noises[ii + 1] = stochastics
234                    .get_mut(msr_type)
235                    .ok_or(ODError::NoiseNotConfigured {
236                        kind: format!("{msr_type:?}"),
237                    })?
238                    .sample(epoch, rng);
239            }
240        }
241
242        Ok(noises)
243    }
244
245    fn available_data(&self) -> u8 {
246        let mut bits: u8 = 0;
247
248        if self.integration_time.is_some() {
249            bits |= 1 << 0;
250        }
251        if self.timestamp_noise_s.is_some() {
252            bits |= 1 << 1;
253        }
254        if self.stochastic_noises.is_some() {
255            bits |= 1 << 2;
256        }
257        bits
258    }
259}
260
261#[cfg(feature = "python")]
262#[cfg_attr(feature = "python", pymethods)]
263impl GroundStation {
264    /// Decodes an ASN.1 DER encoded byte array into a GroundStation object.
265    ///
266    /// :type data: bytes
267    /// :rtype: GroundStation
268    #[classmethod]
269    pub fn from_asn1(_cls: &Bound<'_, PyType>, data: &[u8]) -> PyResult<Self> {
270        match Self::from_der(data) {
271            Ok(obj) => Ok(obj),
272            Err(e) => Err(PyValueError::new_err(format!("ASN.1 decoding error: {e}"))),
273        }
274    }
275
276    /// Encodes this GroundStation object into an ASN.1 DER encoded byte array.
277    ///
278    /// :rtype: bytes
279    pub fn to_asn1<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
280        let mut buf = Vec::new();
281        match self.encode_to_vec(&mut buf) {
282            Ok(_) => Ok(PyBytes::new(py, &buf)),
283            Err(e) => Err(PyValueError::new_err(format!("ASN.1 encoding error: {e}"))),
284        }
285    }
286}
287
288impl Default for GroundStation {
289    fn default() -> Self {
290        let mut measurement_types = IndexSet::new();
291        measurement_types.insert(MeasurementType::Range);
292        measurement_types.insert(MeasurementType::Doppler);
293        Self {
294            name: "UNDEFINED".to_string(),
295            measurement_types,
296            location: Location::default(),
297            integration_time: None,
298            light_time_correction: false,
299            timestamp_noise_s: None,
300            stochastic_noises: None,
301        }
302    }
303}
304
305impl ConfigRepr for GroundStation {}
306
307#[derive(der::Sequence)]
308struct MsrNoisePair {
309    msr_type: MeasurementType,
310    noise: StochasticNoise,
311}
312
313impl<'a> Decode<'a> for GroundStation {
314    fn decode<R: Reader<'a>>(decoder: &mut R) -> der::Result<Self> {
315        let name: String = decoder.decode()?;
316        let location = decoder.decode()?;
317        // Measurement types are stored as a sequence of measurement types
318        let msr_types_vec: Vec<MeasurementType> = decoder.decode()?;
319        let measurement_types = IndexSet::from_iter(msr_types_vec);
320
321        let light_time_correction = decoder.decode()?;
322
323        // The flags tell us what happens next
324        let flags: u8 = decoder.decode()?;
325
326        let integration_time = if flags & (1 << 0) != 0 {
327            Some(Duration::from_total_nanoseconds(decoder.decode()?))
328        } else {
329            None
330        };
331
332        let timestamp_noise_s = if flags & (1 << 1) != 0 {
333            Some(decoder.decode()?)
334        } else {
335            None
336        };
337
338        let stochastic_noises = if flags & (1 << 2) != 0 {
339            // Stochastic noises are stored as a sequence of (MeasurementType, StochasticNoise) tuples (SEQUENCE of SEQUENCE)
340            // We define a helper struct for decoding
341
342            let stochastics_vec: Vec<MsrNoisePair> = decoder.decode()?;
343            let mut map = IndexMap::new();
344            for pair in stochastics_vec {
345                map.insert(pair.msr_type, pair.noise);
346            }
347            Some(map)
348        } else {
349            None
350        };
351
352        Ok(GroundStation {
353            name,
354            location,
355            measurement_types,
356            integration_time,
357            light_time_correction,
358            timestamp_noise_s,
359            stochastic_noises,
360        })
361    }
362}
363
364impl Encode for GroundStation {
365    fn encoded_len(&self) -> der::Result<der::Length> {
366        let msr_types_vec: Vec<MeasurementType> = self.measurement_types.iter().copied().collect();
367
368        let integration_time_ns = self.integration_time.map(|d| d.total_nanoseconds());
369
370        let stochastics_vec = self.stochastic_noises.as_ref().map(|map| {
371            map.iter()
372                .map(|(k, v)| MsrNoisePair {
373                    msr_type: *k,
374                    noise: *v,
375                })
376                .collect::<Vec<MsrNoisePair>>()
377        });
378
379        self.name.encoded_len()?
380            + self.location.encoded_len()?
381            + msr_types_vec.encoded_len()?
382            + self.light_time_correction.encoded_len()?
383            + self.available_data().encoded_len()?
384            + integration_time_ns.encoded_len()?
385            + self.timestamp_noise_s.encoded_len()?
386            + stochastics_vec.encoded_len()?
387    }
388
389    fn encode(&self, encoder: &mut impl der::Writer) -> der::Result<()> {
390        self.name.encode(encoder)?;
391        self.location.encode(encoder)?;
392
393        let msr_types_vec: Vec<MeasurementType> = self.measurement_types.iter().copied().collect();
394        msr_types_vec.encode(encoder)?;
395
396        self.light_time_correction.encode(encoder)?;
397        self.available_data().encode(encoder)?;
398
399        let integration_time_ns = self.integration_time.map(|d| d.total_nanoseconds());
400        integration_time_ns.encode(encoder)?;
401
402        self.timestamp_noise_s.encode(encoder)?;
403
404        let stochastics_vec = self.stochastic_noises.as_ref().map(|map| {
405            map.iter()
406                .map(|(k, v)| MsrNoisePair {
407                    msr_type: *k,
408                    noise: *v,
409                })
410                .collect::<Vec<MsrNoisePair>>()
411        });
412        stochastics_vec.encode(encoder)?;
413
414        Ok(())
415    }
416}
417
418impl fmt::Display for GroundStation {
419    // Prints the Keplerian orbital elements with units
420    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
421        write!(f, "{} ({})", self.name, self.location)
422    }
423}
424
425#[cfg(test)]
426mod gs_ut {
427
428    use anise::astro::{Location, TerrainMask};
429    use anise::constants::frames::IAU_EARTH_FRAME;
430    use indexmap::{IndexMap, IndexSet};
431
432    use crate::io::ConfigRepr;
433    use crate::od::prelude::*;
434
435    #[test]
436    fn test_load_single() {
437        use std::env;
438        use std::path::PathBuf;
439
440        use hifitime::TimeUnits;
441
442        let test_data: PathBuf = [
443            env!("CARGO_MANIFEST_DIR"),
444            "../data",
445            "03_tests",
446            "config",
447            "one_ground_station.yaml",
448        ]
449        .iter()
450        .collect();
451
452        assert!(test_data.exists(), "Could not find the test data");
453
454        let gs = GroundStation::load(test_data).unwrap();
455
456        dbg!(&gs);
457
458        let mut measurement_types = IndexSet::new();
459        measurement_types.insert(MeasurementType::Range);
460        measurement_types.insert(MeasurementType::Doppler);
461
462        let mut stochastics = IndexMap::new();
463        stochastics.insert(
464            MeasurementType::Range,
465            StochasticNoise {
466                bias: Some(GaussMarkov::new(1.days(), 5e-3).unwrap()),
467                ..Default::default()
468            },
469        );
470        stochastics.insert(
471            MeasurementType::Doppler,
472            StochasticNoise {
473                bias: Some(GaussMarkov::new(1.days(), 5e-5).unwrap()),
474                ..Default::default()
475            },
476        );
477
478        let expected_gs = GroundStation {
479            name: "Demo ground station".to_string(),
480            location: Location {
481                latitude_deg: 2.3522,
482                longitude_deg: 48.8566,
483                height_km: 0.4,
484                frame: IAU_EARTH_FRAME.into(),
485                terrain_mask: TerrainMask::from_flat_terrain(5.0),
486                terrain_mask_ignored: false,
487            },
488            measurement_types,
489            stochastic_noises: Some(stochastics),
490
491            light_time_correction: false,
492            timestamp_noise_s: None,
493            integration_time: Some(60 * Unit::Second),
494        };
495
496        println!("{}", serde_yml::to_string(&expected_gs).unwrap());
497
498        assert_eq!(expected_gs, gs);
499    }
500
501    #[test]
502    fn test_load_many() {
503        use hifitime::TimeUnits;
504        use std::env;
505        use std::path::PathBuf;
506
507        let test_file: PathBuf = [
508            env!("CARGO_MANIFEST_DIR"),
509            "../data",
510            "03_tests",
511            "config",
512            "many_ground_stations.yaml",
513        ]
514        .iter()
515        .collect();
516
517        let stations = GroundStation::load_many(test_file).unwrap();
518
519        dbg!(&stations);
520
521        let mut measurement_types = IndexSet::new();
522        measurement_types.insert(MeasurementType::Range);
523        measurement_types.insert(MeasurementType::Doppler);
524
525        let mut stochastics = IndexMap::new();
526        stochastics.insert(
527            MeasurementType::Range,
528            StochasticNoise {
529                bias: Some(GaussMarkov::new(1.days(), 5e-3).unwrap()),
530                ..Default::default()
531            },
532        );
533        stochastics.insert(
534            MeasurementType::Doppler,
535            StochasticNoise {
536                bias: Some(GaussMarkov::new(1.days(), 5e-5).unwrap()),
537                ..Default::default()
538            },
539        );
540
541        let expected = vec![
542            GroundStation {
543                name: "Demo ground station".to_string(),
544                location: Location {
545                    latitude_deg: 2.3522,
546                    longitude_deg: 48.8566,
547                    height_km: 0.4,
548                    frame: IAU_EARTH_FRAME.into(),
549                    terrain_mask: TerrainMask::from_flat_terrain(5.0),
550                    terrain_mask_ignored: false,
551                },
552                measurement_types: measurement_types.clone(),
553                stochastic_noises: Some(stochastics.clone()),
554                light_time_correction: false,
555                timestamp_noise_s: None,
556                integration_time: None,
557            },
558            GroundStation {
559                name: "Canberra".to_string(),
560                location: Location {
561                    latitude_deg: -35.398333,
562                    longitude_deg: 148.981944,
563                    height_km: 0.691750,
564                    frame: IAU_EARTH_FRAME.into(),
565                    terrain_mask: TerrainMask::from_flat_terrain(5.0),
566                    terrain_mask_ignored: false,
567                },
568                measurement_types,
569                stochastic_noises: Some(stochastics),
570                light_time_correction: false,
571                timestamp_noise_s: None,
572                integration_time: None,
573            },
574        ];
575
576        assert_eq!(expected, stations);
577
578        // Serialize back
579        let reser = serde_yml::to_string(&expected).unwrap();
580        dbg!(reser);
581    }
582}