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