Skip to main content

nyx_space/od/simulator/
trkconfig.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::scheduler::Scheduler;
20use crate::io::ConfigRepr;
21use crate::io::{ConfigError, duration_from_str, duration_to_str, epoch_from_str, epoch_to_str};
22use der::{Decode, Encode, Reader};
23use hifitime::TimeUnits;
24use hifitime::{Duration, Epoch, TimeScale};
25use serde::Deserialize;
26use serde::Serialize;
27use std::fmt;
28use std::fmt::Debug;
29use std::str::FromStr;
30use typed_builder::TypedBuilder;
31
32#[cfg(feature = "python")]
33use pyo3::{exceptions::PyValueError, prelude::*, types::PyBytes, types::PyType};
34
35/// Stores a tracking configuration, there is one per tracking data simulator (e.g. one for ground station #1 and another for #2).
36/// By default, the tracking configuration is continuous and the tracking arc is from the beginning of the simulation to the end.
37/// In Python, any value that is set to None at initialization will use the default values: no scheduler, no strands, sampling at 1 min.
38///
39/// :type scheduler: Scheduler | None
40/// :type sampling: Duration
41/// :type strands: list[Strand] | None
42#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, TypedBuilder)]
43#[cfg_attr(feature = "python", pyclass(from_py_object, get_all, set_all))]
44#[builder(doc)]
45pub struct TrkConfig {
46    /// Set to automatically build a tracking schedule based on some criteria
47    #[serde(default)]
48    #[builder(default, setter(strip_option))]
49    pub scheduler: Option<Scheduler>,
50    #[serde(
51        serialize_with = "duration_to_str",
52        deserialize_with = "duration_from_str"
53    )]
54    /// Sampling rate once tracking has started
55    #[builder(default = 1.minutes())]
56    pub sampling: Duration,
57    /// List of tracking strands during which the given tracker will be tracking
58    #[builder(default, setter(strip_option))]
59    pub strands: Option<Vec<Strand>>,
60}
61
62impl<'a> Decode<'a> for TrkConfig {
63    fn decode<R: Reader<'a>>(decoder: &mut R) -> der::Result<Self> {
64        let scheduler = if decoder.decode::<bool>()? {
65            Some(decoder.decode()?)
66        } else {
67            None
68        };
69        let sampling_ns = decoder.decode::<i128>()?;
70        let strands = if decoder.decode::<bool>()? {
71            Some(decoder.decode()?)
72        } else {
73            None
74        };
75
76        Ok(Self {
77            scheduler,
78            sampling: Duration::from_total_nanoseconds(sampling_ns),
79            strands,
80        })
81    }
82}
83
84impl Encode for TrkConfig {
85    fn encoded_len(&self) -> der::Result<der::Length> {
86        let mut len = self.scheduler.is_some().encoded_len()?;
87        if let Some(sched) = &self.scheduler {
88            len = (len + sched.encoded_len()?)?;
89        }
90        len = (len + self.sampling.total_nanoseconds().encoded_len()?)?;
91        len = (len + self.strands.is_some().encoded_len()?)?;
92        if let Some(strands) = &self.strands {
93            len = (len + strands.encoded_len()?)?;
94        }
95        Ok(len)
96    }
97
98    fn encode(&self, encoder: &mut impl der::Writer) -> der::Result<()> {
99        if let Some(sched) = &self.scheduler {
100            true.encode(encoder)?;
101            sched.encode(encoder)?;
102        } else {
103            false.encode(encoder)?;
104        }
105        self.sampling.total_nanoseconds().encode(encoder)?;
106        if let Some(strands) = &self.strands {
107            true.encode(encoder)?;
108            strands.encode(encoder)?;
109        } else {
110            false.encode(encoder)?;
111        }
112        Ok(())
113    }
114}
115
116#[cfg(feature = "python")]
117#[cfg_attr(feature = "python", pymethods)]
118impl TrkConfig {
119    #[new]
120    #[pyo3(signature = (scheduler=None, sampling=1.minutes(), strands=None))]
121    fn py_new(
122        scheduler: Option<Scheduler>,
123        sampling: Duration,
124        strands: Option<Vec<Strand>>,
125    ) -> Self {
126        Self {
127            scheduler,
128            sampling,
129            strands,
130        }
131    }
132
133    fn __repr__(&self) -> String {
134        format!("{self:?}")
135    }
136
137    fn __str__(&self) -> String {
138        format!("{self:?}")
139    }
140
141    /// Decodes an ASN.1 DER encoded byte array into a TrkConfig object.
142    ///
143    /// :type data: bytes
144    /// :rtype: TrkConfig
145    #[classmethod]
146    pub fn from_asn1(_cls: &Bound<'_, PyType>, data: &[u8]) -> PyResult<Self> {
147        match Self::from_der(data) {
148            Ok(obj) => Ok(obj),
149            Err(e) => Err(PyValueError::new_err(format!("ASN.1 decoding error: {e}"))),
150        }
151    }
152
153    /// Encodes this TrkConfig object into an ASN.1 DER encoded byte array.
154    ///
155    /// :rtype: bytes
156    pub fn to_asn1<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
157        let mut buf = Vec::new();
158        match self.encode_to_vec(&mut buf) {
159            Ok(_) => Ok(PyBytes::new(py, &buf)),
160            Err(e) => Err(PyValueError::new_err(format!("ASN.1 encoding error: {e}"))),
161        }
162    }
163}
164
165impl ConfigRepr for TrkConfig {}
166
167impl FromStr for TrkConfig {
168    type Err = ConfigError;
169
170    fn from_str(s: &str) -> Result<Self, Self::Err> {
171        serde_yml::from_str(s).map_err(|source| ConfigError::ParseError { source })
172    }
173}
174
175impl TrkConfig {
176    /// Initialize a default TrkConfig providing only the sample rate.
177    /// Note: this will also set the sample alignment time to the provided duration.
178    pub fn from_sample_rate(sampling: Duration) -> Self {
179        Self {
180            sampling,
181            scheduler: Some(Scheduler::builder().sample_alignment(sampling).build()),
182            ..Default::default()
183        }
184    }
185
186    /// Check that the configuration is valid: a successful call means that either we have a set of tracking strands or we have a valid scheduler
187    pub(crate) fn sanity_check(&self) -> Result<(), ConfigError> {
188        if self.strands.is_some() && self.scheduler.is_some() {
189            return Err(ConfigError::InvalidConfig {
190                msg:
191                    "Both tracking strands and a scheduler are configured, must be one or the other"
192                        .to_string(),
193            });
194        } else if let Some(strands) = &self.strands {
195            if strands.is_empty() && self.scheduler.is_none() {
196                return Err(ConfigError::InvalidConfig {
197                    msg: "Provided tracking strands is empty and no scheduler is defined"
198                        .to_string(),
199                });
200            }
201            for (ii, strand) in strands.iter().enumerate() {
202                if strand.duration() < self.sampling {
203                    return Err(ConfigError::InvalidConfig {
204                        msg: format!(
205                            "Strand #{ii} lasts {} which is shorter than sampling time of {}",
206                            strand.duration(),
207                            self.sampling
208                        ),
209                    });
210                }
211                if strand.duration().is_negative() {
212                    return Err(ConfigError::InvalidConfig {
213                        msg: format!("Strand #{ii} is anti-chronological"),
214                    });
215                }
216            }
217        } else if self.strands.is_none() && self.scheduler.is_none() {
218            return Err(ConfigError::InvalidConfig {
219                msg: "Neither tracking strands not a scheduler is provided".to_string(),
220            });
221        }
222
223        Ok(())
224    }
225}
226
227impl fmt::Display for TrkConfig {
228    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229        write!(f, "Sampling rate: {}", self.sampling)?;
230
231        match (&self.scheduler, &self.strands) {
232            (Some(sched), None) => {
233                write!(f, " | Mode: Auto-scheduler active ({:?})", sched)
234            }
235            (None, Some(strands)) => {
236                write!(f, " | Mode: Executing {} explicit strand(s)", strands.len())
237            }
238            (Some(sched), Some(strands)) => write!(
239                f,
240                " | CONFIG ERROR: Conflicting state (Scheduler {:?} AND {} strands)",
241                sched,
242                strands.len()
243            ),
244            (None, None) => write!(
245                f,
246                " | CONFIG ERROR: Invalid state (Neither scheduler nor strands defined)"
247            ),
248        }
249    }
250}
251
252impl Default for TrkConfig {
253    /// The default configuration is to generate a measurement every minute (continuously) while the vehicle is visible
254    fn default() -> Self {
255        Self {
256            // Allows calling the builder's defaults
257            scheduler: Some(Scheduler::builder().build()),
258            sampling: 1.minutes(),
259            strands: None,
260        }
261    }
262}
263
264/// Stores a tracking strand with a start and end epoch
265///
266/// :type start: Epoch
267/// :type end: Epoch
268#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
269#[cfg_attr(feature = "python", pyclass(from_py_object, get_all, set_all))]
270pub struct Strand {
271    #[serde(serialize_with = "epoch_to_str", deserialize_with = "epoch_from_str")]
272    pub start: Epoch,
273    #[serde(serialize_with = "epoch_to_str", deserialize_with = "epoch_from_str")]
274    pub end: Epoch,
275}
276
277impl fmt::Display for Strand {
278    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
279        write!(
280            f,
281            "[{}, {}] (Δt: {})",
282            self.start,
283            self.end,
284            self.duration()
285        )
286    }
287}
288
289impl<'a> Decode<'a> for Strand {
290    fn decode<R: Reader<'a>>(decoder: &mut R) -> der::Result<Self> {
291        let start_ns = decoder.decode::<i128>()?;
292        let start_ts_u8 = decoder.decode::<u8>()?;
293        let start_ts = TimeScale::from(start_ts_u8);
294
295        let end_ns = decoder.decode::<i128>()?;
296        let end_ts_u8 = decoder.decode::<u8>()?;
297        let end_ts = TimeScale::from(end_ts_u8);
298
299        Ok(Self {
300            start: Epoch::from_duration(Duration::from_total_nanoseconds(start_ns), start_ts),
301            end: Epoch::from_duration(Duration::from_total_nanoseconds(end_ns), end_ts),
302        })
303    }
304}
305
306impl Encode for Strand {
307    fn encoded_len(&self) -> der::Result<der::Length> {
308        let ts_len = 1u8.encoded_len()?;
309        let start_len = (self.start.duration.total_nanoseconds().encoded_len()? + ts_len)?;
310        let end_len = (self.end.duration.total_nanoseconds().encoded_len()? + ts_len)?;
311        start_len + end_len
312    }
313
314    fn encode(&self, encoder: &mut impl der::Writer) -> der::Result<()> {
315        self.start.duration.total_nanoseconds().encode(encoder)?;
316        (self.start.time_scale as u8).encode(encoder)?;
317
318        self.end.duration.total_nanoseconds().encode(encoder)?;
319        (self.end.time_scale as u8).encode(encoder)
320    }
321}
322
323impl Strand {
324    pub fn new(start: Epoch, end: Epoch) -> Self {
325        Self { start, end }
326    }
327
328    /// Returns whether the provided epoch is within the range
329    pub fn contains(&self, epoch: Epoch) -> bool {
330        (self.start..=self.end).contains(&epoch)
331    }
332
333    /// Returns the duration of this tracking strand
334    pub fn duration(&self) -> Duration {
335        self.end - self.start
336    }
337}
338
339#[cfg(feature = "python")]
340#[cfg_attr(feature = "python", pymethods)]
341impl Strand {
342    #[new]
343    fn py_new(start: Epoch, end: Epoch) -> Self {
344        Self::new(start, end)
345    }
346
347    fn __repr__(&self) -> String {
348        format!("{self:?}")
349    }
350
351    fn __str__(&self) -> String {
352        format!("{self:?}")
353    }
354
355    /// Decodes an ASN.1 DER encoded byte array into a Strand object.
356    ///
357    /// :type data: bytes
358    /// :rtype: Strand
359    #[classmethod]
360    pub fn from_asn1(_cls: &Bound<'_, PyType>, data: &[u8]) -> PyResult<Self> {
361        match Self::from_der(data) {
362            Ok(obj) => Ok(obj),
363            Err(e) => Err(PyValueError::new_err(format!("ASN.1 decoding error: {e}"))),
364        }
365    }
366
367    /// Encodes this Strand object into an ASN.1 DER encoded byte array.
368    ///
369    /// :rtype: bytes
370    pub fn to_asn1<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
371        let mut buf = Vec::new();
372        match self.encode_to_vec(&mut buf) {
373            Ok(_) => Ok(PyBytes::new(py, &buf)),
374            Err(e) => Err(PyValueError::new_err(format!("ASN.1 encoding error: {e}"))),
375        }
376    }
377}
378
379#[cfg(test)]
380mod trkconfig_ut {
381    use crate::io::ConfigRepr;
382    use crate::od::simulator::{Cadence, Handoff, Scheduler, Strand, TrkConfig};
383    use der::{Decode, Encode};
384    use hifitime::{Epoch, TimeUnits};
385
386    #[test]
387    fn sanity_checks() {
388        let mut cfg = TrkConfig::default();
389        assert!(cfg.sanity_check().is_ok(), "default config should be sane");
390
391        cfg.scheduler = None;
392        assert!(
393            cfg.sanity_check().is_err(),
394            "no scheduler should mark this insane"
395        );
396
397        cfg.strands = Some(Vec::new());
398        assert!(
399            cfg.sanity_check().is_err(),
400            "no scheduler and empty strands should mark this insane"
401        );
402
403        let start = Epoch::now().unwrap();
404        let end = start + 10.seconds();
405        cfg.strands = Some(vec![Strand { start, end }]);
406        assert!(
407            cfg.sanity_check().is_err(),
408            "strand of too short of a duration should mark this insane"
409        );
410
411        let end = start + cfg.sampling;
412        cfg.strands = Some(vec![Strand { start, end }]);
413        assert!(
414            cfg.sanity_check().is_ok(),
415            "strand allowing for a single measurement should be OK"
416        );
417
418        // An anti-chronological strand should be invalid
419        cfg.strands = Some(vec![Strand {
420            start: end,
421            end: start,
422        }]);
423        assert!(
424            cfg.sanity_check().is_err(),
425            "anti chronological strand should be insane"
426        );
427    }
428
429    #[test]
430    fn serde_trkconfig() {
431        use serde_yml;
432
433        // Test the default config
434        let cfg = TrkConfig::default();
435        let serialized = serde_yml::to_string(&cfg).unwrap();
436        println!("{serialized}");
437        let deserd: TrkConfig = serde_yml::from_str(&serialized).unwrap();
438        assert_eq!(deserd, cfg);
439        assert_eq!(
440            cfg.scheduler.unwrap(),
441            Scheduler::builder().min_samples(10).build()
442        );
443        assert!(cfg.strands.is_none());
444
445        // Specify an intermittent schedule and a specific start epoch.
446        let cfg = TrkConfig {
447            scheduler: Some(Scheduler {
448                cadence: Cadence::Intermittent {
449                    on: 23.1.hours(),
450                    off: 0.9.hours(),
451                },
452                handoff: Handoff::Eager,
453                min_samples: 10,
454                ..Default::default()
455            }),
456            sampling: 45.2.seconds(),
457            ..Default::default()
458        };
459        let serialized = serde_yml::to_string(&cfg).unwrap();
460        println!("{serialized}");
461        let deserd: TrkConfig = serde_yml::from_str(&serialized).unwrap();
462        assert_eq!(deserd, cfg);
463    }
464
465    #[test]
466    fn deserialize_from_file() {
467        use std::collections::BTreeMap;
468        use std::env;
469        use std::path::PathBuf;
470
471        // Load the tracking configuration from the test data.
472        let trkconfg_yaml: PathBuf = [
473            env!("CARGO_MANIFEST_DIR"),
474            "../data",
475            "03_tests",
476            "config",
477            "tracking_cfg.yaml",
478        ]
479        .iter()
480        .collect();
481
482        let configs: BTreeMap<String, TrkConfig> = TrkConfig::load_named(trkconfg_yaml).unwrap();
483        dbg!(configs);
484    }
485
486    #[test]
487    fn api_trk_config() {
488        use serde_yml;
489
490        let cfg = TrkConfig::builder()
491            .sampling(15.seconds())
492            .scheduler(Scheduler::builder().handoff(Handoff::Overlap).build())
493            .build();
494
495        let serialized = serde_yml::to_string(&cfg).unwrap();
496        println!("{serialized}");
497        let deserd: TrkConfig = serde_yml::from_str(&serialized).unwrap();
498        assert_eq!(deserd, cfg);
499
500        let cfg = TrkConfig::builder()
501            .scheduler(Scheduler::builder().handoff(Handoff::Overlap).build())
502            .build();
503
504        assert_eq!(cfg.sampling, 60.seconds());
505    }
506
507    #[test]
508    fn test_handoff_asn1() {
509        let h = Handoff::Greedy;
510        let mut buf = Vec::new();
511        h.encode_to_vec(&mut buf).unwrap();
512        let h2 = Handoff::from_der(&buf).unwrap();
513        assert_eq!(h, h2);
514    }
515
516    #[test]
517    fn test_cadence_asn1() {
518        let c = Cadence::Intermittent {
519            on: 1.0.hours(),
520            off: 0.5.hours(),
521        };
522        let mut buf = Vec::new();
523        c.encode_to_vec(&mut buf).unwrap();
524        let c2 = Cadence::from_der(&buf).unwrap();
525        assert_eq!(c, c2);
526
527        let c = Cadence::Continuous;
528        let mut buf = Vec::new();
529        c.encode_to_vec(&mut buf).unwrap();
530        let c2 = Cadence::from_der(&buf).unwrap();
531        assert_eq!(c, c2);
532    }
533
534    #[test]
535    fn test_scheduler_asn1() {
536        let s = Scheduler::builder()
537            .handoff(Handoff::Overlap)
538            .cadence(Cadence::Intermittent {
539                on: 10.0.minutes(),
540                off: 5.0.minutes(),
541            })
542            .min_samples(5)
543            .sample_alignment(1.0.seconds())
544            .build();
545
546        let mut buf = Vec::new();
547        s.encode_to_vec(&mut buf).unwrap();
548        let s2 = Scheduler::from_der(&buf).unwrap();
549        assert_eq!(s, s2);
550    }
551
552    #[test]
553    fn test_strand_asn1() {
554        let epoch = Epoch::from_gregorian_utc_at_midnight(2023, 1, 1);
555        let s = Strand {
556            start: epoch,
557            end: epoch + 1.0.hours(),
558        };
559
560        let mut buf = Vec::new();
561        s.encode_to_vec(&mut buf).unwrap();
562        let s2 = Strand::from_der(&buf).unwrap();
563
564        assert_eq!(s, s2);
565
566        // Test TAI explicitly
567        let epoch_tai = Epoch::from_gregorian_utc_at_midnight(2023, 1, 1);
568        let s = Strand {
569            start: epoch_tai,
570            end: epoch_tai + 1.0.hours(),
571        };
572
573        let mut buf = Vec::new();
574        s.encode_to_vec(&mut buf).unwrap();
575        let s2 = Strand::from_der(&buf).unwrap();
576
577        assert_eq!(s, s2);
578    }
579
580    #[test]
581    fn test_trkconfig_asn1() {
582        // Encode one in UTC and the other in TAI
583        let epoch = Epoch::from_gregorian_utc_at_midnight(2023, 1, 1);
584        let strand = Strand {
585            start: epoch,
586            end: (epoch + 1.0.hours()).to_time_scale(hifitime::TimeScale::TAI),
587        };
588
589        let cfg = TrkConfig::builder()
590            .sampling(10.0.seconds())
591            .strands(vec![strand])
592            .build();
593
594        let mut buf = Vec::new();
595        cfg.encode_to_vec(&mut buf).unwrap();
596        let cfg2 = TrkConfig::from_der(&buf).unwrap();
597
598        assert_eq!(cfg, cfg2);
599    }
600}