nyx_space/od/simulator/
trkconfig.rs1use 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#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, TypedBuilder)]
35#[builder(doc)]
36pub struct TrkConfig {
37 #[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 #[builder(default = 1.minutes())]
47 pub sampling: Duration,
48 #[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 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 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 fn default() -> Self {
118 Self {
119 scheduler: Some(Scheduler::builder().build()),
121 sampling: 1.minutes(),
122 strands: None,
123 }
124 }
125}
126
127#[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 pub fn contains(&self, epoch: Epoch) -> bool {
139 (self.start..=self.end).contains(&epoch)
140 }
141
142 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 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 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 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 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}