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