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