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 hifitime::TimeUnits;
23use hifitime::{Duration, Epoch};
24
25use serde::Deserialize;
26use serde_derive::Serialize;
27use std::fmt::Debug;
28use std::str::FromStr;
29use typed_builder::TypedBuilder;
30
31/// Stores a tracking configuration, there is one per tracking data simulator (e.g. one for ground station #1 and another for #2).
32/// By default, the tracking configuration is continuous and the tracking arc is from the beginning of the simulation to the end.
33/// In Python, any value that is set to None at initialization will use the default values.
34#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, TypedBuilder)]
35#[builder(doc)]
36pub struct TrkConfig {
37    /// Set to automatically build a tracking schedule based on some criteria
38    #[serde(default)]
39    #[builder(default, setter(strip_option))]
40    pub scheduler: Option<Scheduler>,
41    #[serde(
42        serialize_with = "duration_to_str",
43        deserialize_with = "duration_from_str"
44    )]
45    /// Sampling rate once tracking has started
46    #[builder(default = 1.minutes())]
47    pub sampling: Duration,
48    /// List of tracking strands during which the given tracker will be tracking
49    #[builder(default, setter(strip_option))]
50    pub strands: Option<Vec<Strand>>,
51}
52
53impl ConfigRepr for TrkConfig {}
54
55impl FromStr for TrkConfig {
56    type Err = ConfigError;
57
58    fn from_str(s: &str) -> Result<Self, Self::Err> {
59        serde_yml::from_str(s).map_err(|source| ConfigError::ParseError { source })
60    }
61}
62
63impl TrkConfig {
64    /// Initialize a default TrkConfig providing only the sample rate.
65    /// Note: this will also set the sample alignment time to the provided duration.
66    pub fn from_sample_rate(sampling: Duration) -> Self {
67        Self {
68            sampling,
69            scheduler: Some(Scheduler::builder().sample_alignment(sampling).build()),
70            ..Default::default()
71        }
72    }
73
74    /// 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
75    pub(crate) fn sanity_check(&self) -> Result<(), ConfigError> {
76        if self.strands.is_some() && self.scheduler.is_some() {
77            return Err(ConfigError::InvalidConfig {
78                msg:
79                    "Both tracking strands and a scheduler are configured, must be one or the other"
80                        .to_string(),
81            });
82        } else if let Some(strands) = &self.strands {
83            if strands.is_empty() && self.scheduler.is_none() {
84                return Err(ConfigError::InvalidConfig {
85                    msg: "Provided tracking strands is empty and no scheduler is defined"
86                        .to_string(),
87                });
88            }
89            for (ii, strand) in strands.iter().enumerate() {
90                if strand.duration() < self.sampling {
91                    return Err(ConfigError::InvalidConfig {
92                        msg: format!(
93                            "Strand #{ii} lasts {} which is shorter than sampling time of {}",
94                            strand.duration(),
95                            self.sampling
96                        ),
97                    });
98                }
99                if strand.duration().is_negative() {
100                    return Err(ConfigError::InvalidConfig {
101                        msg: format!("Strand #{ii} is anti-chronological"),
102                    });
103                }
104            }
105        } else if self.strands.is_none() && self.scheduler.is_none() {
106            return Err(ConfigError::InvalidConfig {
107                msg: "Neither tracking strands not a scheduler is provided".to_string(),
108            });
109        }
110
111        Ok(())
112    }
113}
114
115impl Default for TrkConfig {
116    /// The default configuration is to generate a measurement every minute (continuously) while the vehicle is visible
117    fn default() -> Self {
118        Self {
119            // Allows calling the builder's defaults
120            scheduler: Some(Scheduler::builder().build()),
121            sampling: 1.minutes(),
122            strands: None,
123        }
124    }
125}
126
127/// Stores a tracking strand with a start and end epoch
128#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
129pub struct Strand {
130    #[serde(serialize_with = "epoch_to_str", deserialize_with = "epoch_from_str")]
131    pub start: Epoch,
132    #[serde(serialize_with = "epoch_to_str", deserialize_with = "epoch_from_str")]
133    pub end: Epoch,
134}
135
136impl Strand {
137    /// Returns whether the provided epoch is within the range
138    pub fn contains(&self, epoch: Epoch) -> bool {
139        (self.start..=self.end).contains(&epoch)
140    }
141
142    /// Returns the duration of this tracking strand
143    pub fn duration(&self) -> Duration {
144        self.end - self.start
145    }
146}
147
148#[cfg(test)]
149mod trkconfig_ut {
150    use crate::io::ConfigRepr;
151    use crate::od::prelude::*;
152
153    #[test]
154    fn sanity_checks() {
155        let mut cfg = TrkConfig::default();
156        assert!(cfg.sanity_check().is_ok(), "default config should be sane");
157
158        cfg.scheduler = None;
159        assert!(
160            cfg.sanity_check().is_err(),
161            "no scheduler should mark this insane"
162        );
163
164        cfg.strands = Some(Vec::new());
165        assert!(
166            cfg.sanity_check().is_err(),
167            "no scheduler and empty strands should mark this insane"
168        );
169
170        let start = Epoch::now().unwrap();
171        let end = start + 10.seconds();
172        cfg.strands = Some(vec![Strand { start, end }]);
173        assert!(
174            cfg.sanity_check().is_err(),
175            "strand of too short of a duration should mark this insane"
176        );
177
178        let end = start + cfg.sampling;
179        cfg.strands = Some(vec![Strand { start, end }]);
180        assert!(
181            cfg.sanity_check().is_ok(),
182            "strand allowing for a single measurement should be OK"
183        );
184
185        // An anti-chronological strand should be invalid
186        cfg.strands = Some(vec![Strand {
187            start: end,
188            end: start,
189        }]);
190        assert!(
191            cfg.sanity_check().is_err(),
192            "anti chronological strand should be insane"
193        );
194    }
195
196    #[test]
197    fn serde_trkconfig() {
198        use serde_yml;
199
200        // Test the default config
201        let cfg = TrkConfig::default();
202        let serialized = serde_yml::to_string(&cfg).unwrap();
203        println!("{serialized}");
204        let deserd: TrkConfig = serde_yml::from_str(&serialized).unwrap();
205        assert_eq!(deserd, cfg);
206        assert_eq!(
207            cfg.scheduler.unwrap(),
208            Scheduler::builder().min_samples(10).build()
209        );
210        assert!(cfg.strands.is_none());
211
212        // Specify an intermittent schedule and a specific start epoch.
213        let cfg = TrkConfig {
214            scheduler: Some(Scheduler {
215                cadence: Cadence::Intermittent {
216                    on: 23.1.hours(),
217                    off: 0.9.hours(),
218                },
219                handoff: Handoff::Eager,
220                min_samples: 10,
221                ..Default::default()
222            }),
223            sampling: 45.2.seconds(),
224            ..Default::default()
225        };
226        let serialized = serde_yml::to_string(&cfg).unwrap();
227        println!("{serialized}");
228        let deserd: TrkConfig = serde_yml::from_str(&serialized).unwrap();
229        assert_eq!(deserd, cfg);
230    }
231
232    #[test]
233    fn deserialize_from_file() {
234        use std::collections::BTreeMap;
235        use std::env;
236        use std::path::PathBuf;
237
238        // Load the tracking configuration from the test data.
239        let trkconfg_yaml: PathBuf = [
240            &env::var("CARGO_MANIFEST_DIR").unwrap(),
241            "data",
242            "tests",
243            "config",
244            "tracking_cfg.yaml",
245        ]
246        .iter()
247        .collect();
248
249        let configs: BTreeMap<String, TrkConfig> = TrkConfig::load_named(trkconfg_yaml).unwrap();
250        dbg!(configs);
251    }
252
253    #[test]
254    fn api_trk_config() {
255        use serde_yml;
256
257        let cfg = TrkConfig::builder()
258            .sampling(15.seconds())
259            .scheduler(Scheduler::builder().handoff(Handoff::Overlap).build())
260            .build();
261
262        let serialized = serde_yml::to_string(&cfg).unwrap();
263        println!("{serialized}");
264        let deserd: TrkConfig = serde_yml::from_str(&serialized).unwrap();
265        assert_eq!(deserd, cfg);
266
267        let cfg = TrkConfig::builder()
268            .scheduler(Scheduler::builder().handoff(Handoff::Overlap).build())
269            .build();
270
271        assert_eq!(cfg.sampling, 60.seconds());
272    }
273}