Skip to main content

nyx_space/od/simulator/
scheduler.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 crate::io::{
20    duration_from_str, duration_to_str, maybe_duration_from_str, maybe_duration_to_str,
21};
22pub use crate::State;
23use der::{Decode, Encode, Enumerated, Reader};
24use hifitime::{Duration, Unit};
25use serde::Deserialize;
26use serde::Serialize;
27use std::fmt::Debug;
28use typed_builder::TypedBuilder;
29
30#[cfg(feature = "python")]
31use pyo3::{exceptions::PyValueError, prelude::*, types::PyBytes, types::PyType};
32
33/// Defines the handoff from a current ground station to the next one that is visible to prevent overlapping of measurements
34#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Serialize, Default, Enumerated)]
35#[repr(u8)]
36#[cfg_attr(feature = "python", pyclass(eq, eq_int))]
37pub enum Handoff {
38    /// If a new station is in visibility of the spacecraft, the "Eager" station will immediately stop tracking and switch over (default)
39    #[default]
40    Eager = 0,
41    /// If a new station is in visibility of the spacecraft, the "Greedy" station will continue to tracking until the vehicle is below its elevation mask
42    Greedy = 1,
43    /// If a new station is in visibility of the spacecraft, the "Overlap" station will continue tracking, and so will the other one
44    Overlap = 2,
45}
46
47#[cfg(feature = "python")]
48#[cfg_attr(feature = "python", pymethods)]
49impl Handoff {
50    fn __repr__(&self) -> String {
51        format!("{self:?}")
52    }
53
54    fn __str__(&self) -> String {
55        format!("{self:?}")
56    }
57}
58
59/// A scheduler allows building a scheduling of spaceraft tracking for a set of ground stations.
60#[derive(Copy, Clone, Debug, Default, Deserialize, PartialEq, Serialize, TypedBuilder)]
61#[cfg_attr(feature = "python", pyclass)]
62#[builder(doc)]
63pub struct Scheduler {
64    /// Handoff strategy if two trackers see the vehicle at the same time
65    #[builder(default)]
66    pub handoff: Handoff,
67    /// On/off cadence of this scheduler
68    #[builder(default)]
69    pub cadence: Cadence,
70    /// Minimum number of samples for a valid arc, i.e. if there are less than this many samples during a pass, the strand is discarded.
71    #[builder(default = 10)]
72    pub min_samples: u32,
73    /// Round the time of the samples to the provided duration. For example, if the vehicle is above the horizon at 01:02:03.456 and the alignment
74    /// is set to 01 seconds, then this will cause the tracking to start at 01:02:03 as it is rounded to the nearest second.
75    #[builder(default = Some(Unit::Second * 1.0), setter(strip_option))]
76    #[serde(
77        serialize_with = "maybe_duration_to_str",
78        deserialize_with = "maybe_duration_from_str"
79    )]
80    pub sample_alignment: Option<Duration>,
81}
82
83/// Determines whether tracking is continuous or intermittent.
84#[derive(Copy, Clone, Deserialize, PartialEq, Serialize, Default)]
85pub enum Cadence {
86    #[default]
87    Continuous,
88    /// An intermittent schedule has On and Off durations.
89    Intermittent {
90        #[serde(
91            serialize_with = "duration_to_str",
92            deserialize_with = "duration_from_str"
93        )]
94        on: Duration,
95        #[serde(
96            serialize_with = "duration_to_str",
97            deserialize_with = "duration_from_str"
98        )]
99        off: Duration,
100    },
101}
102
103#[cfg(feature = "python")]
104#[pyclass(name = "Cadence")]
105#[derive(Clone, Debug)]
106pub struct PyCadence {
107    pub inner: Cadence,
108}
109
110impl<'a> Decode<'a> for Cadence {
111    fn decode<R: Reader<'a>>(decoder: &mut R) -> der::Result<Self> {
112        let tag = decoder.decode::<u8>()?;
113        match tag {
114            0 => Ok(Self::Continuous),
115            1 => {
116                let on_ns = decoder.decode::<i128>()?;
117                let off_ns = decoder.decode::<i128>()?;
118                Ok(Self::Intermittent {
119                    on: Duration::from_total_nanoseconds(on_ns),
120                    off: Duration::from_total_nanoseconds(off_ns),
121                })
122            }
123            _ => Err(der::ErrorKind::Value {
124                tag: der::Tag::Integer,
125            }
126            .into()),
127        }
128    }
129}
130
131impl Encode for Cadence {
132    fn encoded_len(&self) -> der::Result<der::Length> {
133        match self {
134            Self::Continuous => 0u8.encoded_len(),
135            Self::Intermittent { on, off } => {
136                1u8.encoded_len()?
137                    + on.total_nanoseconds().encoded_len()?
138                    + off.total_nanoseconds().encoded_len()?
139            }
140        }
141    }
142
143    fn encode(&self, encoder: &mut impl der::Writer) -> der::Result<()> {
144        match self {
145            Self::Continuous => 0u8.encode(encoder),
146            Self::Intermittent { on, off } => {
147                1u8.encode(encoder)?;
148                on.total_nanoseconds().encode(encoder)?;
149                off.total_nanoseconds().encode(encoder)
150            }
151        }
152    }
153}
154
155#[cfg(feature = "python")]
156#[pymethods]
157impl PyCadence {
158    #[staticmethod]
159    fn continuous() -> Self {
160        Self {
161            inner: Cadence::Continuous,
162        }
163    }
164
165    #[staticmethod]
166    fn intermittent(on: Duration, off: Duration) -> Self {
167        Self {
168            inner: Cadence::Intermittent { on, off },
169        }
170    }
171
172    fn __repr__(&self) -> String {
173        format!("{:?}", self.inner)
174    }
175
176    fn __str__(&self) -> String {
177        format!("{:?}", self.inner)
178    }
179
180    /// Decodes an ASN.1 DER encoded byte array into a Cadence object.
181    ///
182    /// :type data: bytes
183    /// :rtype: Cadence
184    #[classmethod]
185    pub fn from_asn1(_cls: &Bound<'_, PyType>, data: &[u8]) -> PyResult<Self> {
186        match Cadence::from_der(data) {
187            Ok(obj) => Ok(Self { inner: obj }),
188            Err(e) => Err(PyValueError::new_err(format!("ASN.1 decoding error: {e}"))),
189        }
190    }
191
192    /// Encodes this Cadence object into an ASN.1 DER encoded byte array.
193    ///
194    /// :rtype: bytes
195    pub fn to_asn1<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
196        let mut buf = Vec::new();
197        match self.inner.encode_to_vec(&mut buf) {
198            Ok(_) => Ok(PyBytes::new(py, &buf)),
199            Err(e) => Err(PyValueError::new_err(format!("ASN.1 encoding error: {e}"))),
200        }
201    }
202}
203
204impl Debug for Cadence {
205    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
206        match self {
207            Self::Continuous => write!(f, "Continuous"),
208            Self::Intermittent { on, off } => f
209                .debug_struct("Intermittent")
210                .field("on", &format!("{on}"))
211                .field("off", &format!("{off}"))
212                .finish(),
213        }
214    }
215}
216
217impl<'a> Decode<'a> for Scheduler {
218    fn decode<R: Reader<'a>>(decoder: &mut R) -> der::Result<Self> {
219        let handoff = decoder.decode()?;
220        let cadence = decoder.decode()?;
221        let min_samples = decoder.decode()?;
222        let sample_alignment_ns = if decoder.decode::<bool>()? {
223            Some(decoder.decode::<i128>()?)
224        } else {
225            None
226        };
227
228        Ok(Self {
229            handoff,
230            cadence,
231            min_samples,
232            sample_alignment: sample_alignment_ns.map(Duration::from_total_nanoseconds),
233        })
234    }
235}
236
237impl Encode for Scheduler {
238    fn encoded_len(&self) -> der::Result<der::Length> {
239        let mut len = (self.handoff.encoded_len()?
240            + self.cadence.encoded_len()?
241            + self.min_samples.encoded_len()?
242            + self.sample_alignment.is_some().encoded_len()?)?;
243
244        if let Some(sa) = self.sample_alignment {
245            len = (len + sa.total_nanoseconds().encoded_len()?)?;
246        }
247        Ok(len)
248    }
249
250    fn encode(&self, encoder: &mut impl der::Writer) -> der::Result<()> {
251        self.handoff.encode(encoder)?;
252        self.cadence.encode(encoder)?;
253        self.min_samples.encode(encoder)?;
254        if let Some(sa) = self.sample_alignment {
255            true.encode(encoder)?;
256            sa.total_nanoseconds().encode(encoder)?;
257        } else {
258            false.encode(encoder)?;
259        }
260        Ok(())
261    }
262}
263
264#[cfg(feature = "python")]
265#[cfg_attr(feature = "python", pymethods)]
266impl Scheduler {
267    #[new]
268    #[pyo3(signature = (handoff=Handoff::Eager, cadence=None, min_samples=10, sample_alignment=None))]
269    fn py_new(
270        handoff: Handoff,
271        cadence: Option<PyCadence>,
272        min_samples: u32,
273        sample_alignment: Option<Duration>,
274    ) -> Self {
275        Self {
276            handoff,
277            cadence: cadence.map(|c| c.inner).unwrap_or_default(),
278            min_samples,
279            sample_alignment,
280        }
281    }
282
283    #[getter]
284    fn get_handoff(&self) -> Handoff {
285        self.handoff
286    }
287
288    #[setter]
289    fn set_handoff(&mut self, handoff: Handoff) {
290        self.handoff = handoff;
291    }
292
293    #[getter]
294    fn get_cadence(&self) -> PyCadence {
295        PyCadence {
296            inner: self.cadence,
297        }
298    }
299
300    #[setter]
301    fn set_cadence(&mut self, cadence: PyCadence) {
302        self.cadence = cadence.inner;
303    }
304
305    #[getter]
306    fn get_min_samples(&self) -> u32 {
307        self.min_samples
308    }
309
310    #[setter]
311    fn set_min_samples(&mut self, min_samples: u32) {
312        self.min_samples = min_samples;
313    }
314
315    #[getter]
316    fn get_sample_alignment(&self) -> Option<Duration> {
317        self.sample_alignment
318    }
319
320    #[setter]
321    fn set_sample_alignment(&mut self, sample_alignment: Option<Duration>) {
322        self.sample_alignment = sample_alignment;
323    }
324
325    fn __repr__(&self) -> String {
326        format!("{self:?}")
327    }
328
329    fn __str__(&self) -> String {
330        format!("{self:?}")
331    }
332
333    /// Decodes an ASN.1 DER encoded byte array into a Scheduler object.
334    ///
335    /// :type data: bytes
336    /// :rtype: Scheduler
337    #[classmethod]
338    pub fn from_asn1(_cls: &Bound<'_, PyType>, data: &[u8]) -> PyResult<Self> {
339        match Self::from_der(data) {
340            Ok(obj) => Ok(obj),
341            Err(e) => Err(PyValueError::new_err(format!("ASN.1 decoding error: {e}"))),
342        }
343    }
344
345    /// Encodes this Scheduler object into an ASN.1 DER encoded byte array.
346    ///
347    /// :rtype: bytes
348    pub fn to_asn1<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
349        let mut buf = Vec::new();
350        match self.encode_to_vec(&mut buf) {
351            Ok(_) => Ok(PyBytes::new(py, &buf)),
352            Err(e) => Err(PyValueError::new_err(format!("ASN.1 encoding error: {e}"))),
353        }
354    }
355}
356
357#[cfg(test)]
358mod scheduler_ut {
359    use process::simulator::scheduler::Handoff;
360
361    use crate::od::prelude::*;
362
363    use super::Scheduler;
364
365    #[test]
366    fn serde_cadence() {
367        use hifitime::TimeUnits;
368        use serde_yml;
369
370        let cont: Cadence = serde_yml::from_str("!Continuous").unwrap();
371        assert_eq!(cont, Cadence::Continuous);
372
373        let int: Cadence =
374            serde_yml::from_str("!Intermittent {on: 1 h 35 min, off: 15 h 02 min 3 s}").unwrap();
375        assert_eq!(
376            int,
377            Cadence::Intermittent {
378                on: 1.hours() + 35.0.minutes(),
379                off: 15.hours() + 2.minutes() + 3.seconds()
380            }
381        );
382        assert_eq!(
383            format!("{int:?}"),
384            r#"Intermittent { on: "1 h 35 min", off: "15 h 2 min 3 s" }"#
385        );
386
387        let serialized = serde_yml::to_string(&int).unwrap();
388        let deserd: Cadence = serde_yml::from_str(&serialized).unwrap();
389        assert_eq!(deserd, int);
390    }
391
392    #[test]
393    fn api_and_serde_scheduler() {
394        use hifitime::TimeUnits;
395        use serde_yml;
396
397        let scheduler = Scheduler::default();
398        let serialized = serde_yml::to_string(&scheduler).unwrap();
399        assert_eq!(
400            serialized,
401            "handoff: Eager\ncadence: Continuous\nmin_samples: 0\nsample_alignment: null\n"
402        );
403        let deserd: Scheduler = serde_yml::from_str(&serialized).unwrap();
404        assert_eq!(deserd, scheduler);
405
406        let scheduler = Scheduler::builder()
407            .handoff(Handoff::Eager)
408            .cadence(Cadence::Intermittent {
409                on: 0.2.hours(),
410                off: 17.hours() + 5.minutes(),
411            })
412            .build();
413
414        let serialized = serde_yml::to_string(&scheduler).unwrap();
415        assert_eq!(
416            serialized,
417            "handoff: Eager\ncadence: !Intermittent\n  'on': '12 min'\n  'off': '17 h 5 min'\nmin_samples: 10\nsample_alignment: '1 s'\n"
418        );
419        let deserd: Scheduler = serde_yml::from_str(&serialized).unwrap();
420        assert_eq!(deserd, scheduler);
421    }
422
423    #[test]
424    fn defaults() {
425        let sched = Scheduler::default();
426
427        assert_eq!(sched.cadence, Cadence::Continuous);
428
429        assert_eq!(sched.handoff, Handoff::Eager);
430    }
431}