gnss_qc_traits/processing/time/database.rs
1use crate::{
2 merge::{Error as MergeError, Merge},
3 processing::TimeCorrection,
4};
5
6use hifitime::{Epoch, TimeScale, Unit};
7
8#[cfg(doc)]
9use super::Timeshift;
10
11/// [TimeCorrectionsDB] is a [TimeCorrection]s database used by [TimeScale]
12/// monitoring applications and applications that need exact [TimeScale] states at all times.
13/// Our [Timeshift] trait uses it in the precise conversion method.
14/// [TimeCorrectionsDB] has no means to "check" the internal content with respect
15/// to your application, other than the possible verification of the corrections validity (in time).
16/// You are responsible of the database content with respect to the current time and keeping
17/// it up to date. To avoid memory growth in long term applications, we propose:
18/// - [TimeCorrectionsDB::outdate_past] to declare past [TimePolynomial]s as outdated
19/// - and [TimeCorrectionsDB::outdate_weekly] to discard [TimePolynomial]s published before that week
20#[derive(Default, Clone)]
21pub struct TimeCorrectionsDB {
22 /// When strict validity is asserted, we will respect
23 /// the corrections validity strictly. Otherwise, the last
24 /// correction available may be used (propagated) in the future.
25 strict_validity: bool,
26
27 /// [TimeCorrection] database
28 corrections: Vec<TimeCorrection>,
29}
30
31impl TimeCorrectionsDB {
32 /// The database will respect the corrections validity period strictly,
33 /// and will not propose corrections past the last available in time.
34 pub fn strict_validity(&self) -> Self {
35 let mut s = self.clone();
36 s.strict_validity = true;
37 s
38 }
39
40 /// Add a new [TimeCorrection] to the database.
41 /// This does not discard possible [TimeCorrection]s that may apply
42 /// to these timescales.
43 pub fn add(&mut self, correction: TimeCorrection) {
44 self.corrections.push(correction);
45 }
46
47 /// Discard corrections past this [Epoch].
48 /// Corrections must still exist or be provided quickly, for the database
49 /// to remain valid.
50 pub fn outdate_past(&mut self, instant: Epoch) {
51 self.corrections.retain(|poly| poly.ref_epoch > instant);
52 }
53
54 /// Discard corrections published the week before this [Epoch].
55 /// Corrections must still exist or be provided quickly, for the database
56 /// to remain valid.
57 pub fn outdate_weekly(&mut self, instant: Epoch) {
58 let limit = instant - 7.0 * Unit::Week;
59 self.corrections.retain(|poly| poly.ref_epoch > limit);
60 }
61
62 /// [Epoch] interpolation & correction attempt, into desired [TimeScale].
63 pub fn precise_epoch_correction(&self, t: Epoch, target: TimeScale) -> Option<Epoch> {
64 if t.time_scale == target {
65 // nothing to be done!
66 return Some(t);
67 }
68
69 if let Some(poly) = self
70 .corrections
71 .iter()
72 .filter_map(|poly| {
73 if poly.lhs_timescale == t.time_scale && poly.rhs_timescale == target {
74 Some(poly)
75 } else {
76 None
77 }
78 })
79 .min_by_key(|poly| (t - poly.ref_epoch).abs())
80 {
81 let mut applies = poly.applies(t);
82 if !self.strict_validity {
83 applies |= true;
84 }
85
86 if applies {
87 Some(
88 t.precise_timescale_conversion(true, poly.ref_epoch, poly.polynomial, target)
89 .unwrap(),
90 )
91 } else {
92 None
93 }
94 } else if let Some(poly) = self
95 .corrections
96 .iter()
97 .filter_map(|poly| {
98 if poly.lhs_timescale == target && poly.rhs_timescale == t.time_scale {
99 Some(poly)
100 } else {
101 None
102 }
103 })
104 .min_by_key(|poly| (t - poly.ref_epoch).abs())
105 {
106 let mut applies = poly.applies(t);
107 if !self.strict_validity {
108 applies |= true;
109 }
110
111 if applies {
112 Some(
113 t.precise_timescale_conversion(false, poly.ref_epoch, poly.polynomial, target)
114 .unwrap(),
115 )
116 } else {
117 None
118 }
119 } else {
120 // mixed combinations not supported yet
121 None
122 }
123 }
124
125 // else if let Some(poly) = self
126 // .corrections
127 // .iter()
128 // .filter(|poly| {
129 // if poly.lhs_timescale == t.time_scale {
130 // Some(poly)
131 // } else {
132 // None
133 // }
134 // })
135 // .min_by_key(|poly| {
136 // let transposed = t.to_time_scale(poly.lhs_timescale);
137 // transposed - poly.ref_epoch
138 // })
139 // {
140 // // got a forward (1) proposal
141 // if let Some(poly) = self
142 // .corrections
143 // .iter()
144 // .filter(|poly| {
145 // if poly.rhs_timescale == target {
146 // Some(poly)
147 // } else {
148 // None
149 // }
150 // })
151 // .min_by_key(|poly| {
152 // let transposed = t.to_time_scale(poly.lhs_timescale);
153 // transposed - poly.ref_epoch
154 // })
155 // {
156 // // got a forward (2) proposal
157 // } else {
158 // // got a backward (2) proposal
159 // None
160 // }
161 // } else {
162 // None
163 // }
164 // Some(
165 // t.precise_timescale_conversion(true, poly.ref_epoch, poly.polynomial, target)
166 // .unwrap(),
167 // )
168
169 // for lhs_poly in self.corrections.iter() {
170 // for rhs_poly in self.corrections.iter() {
171 // if lhs_poly.lhs_timescale == t.time_scale && rhs_poly.rhs_timescale == target {
172 // // indirect forward transforms
173
174 // // |BDT-GST|=a0_bdt & |GST-GPST|=a1 dt_gst
175 // // GST=BDT-a0_bdt
176 // // BDT-a0 dt_bdt - GPST = a1 dt_gpst
177 // // BDT-GPST (foward indirect) = a1 dt_gpst + a0 dt_bdt
178
179 // let dt_lhs_s = (t.to_time_scale(lhs_poly.lhs_timescale)
180 // - lhs_poly.ref_epoch)
181 // .to_seconds();
182
183 // let dt_rhs_s = (t.to_time_scale(rhs_poly.lhs_timescale)
184 // - rhs_poly.ref_epoch)
185 // .to_seconds();
186
187 // let mut correction = lhs_poly.polynomial.constant.to_seconds()
188 // + lhs_poly.polynomial.rate.to_seconds() * dt_lhs_s
189 // + lhs_poly.polynomial.accel.to_seconds() * dt_lhs_s.powi(2);
190
191 // // println!("correction = {}", correction);
192
193 // correction += rhs_poly.polynomial.constant.to_seconds()
194 // + rhs_poly.polynomial.rate.to_seconds() * dt_rhs_s
195 // + rhs_poly.polynomial.accel.to_seconds() * dt_rhs_s.powi(2);
196
197 // // println!("total correction = {}", correction);
198
199 // return Some(t.to_time_scale(target) - Duration::from_seconds(correction));
200 // } else if lhs_poly.rhs_timescale == t.time_scale
201 // && rhs_poly.rhs_timescale == target
202 // {
203 // // indirect backward + forward transforms
204 // } else if lhs_poly.lhs_timescale == t.time_scale
205 // && rhs_poly.lhs_timescale == target
206 // {
207 // // indirect forward + backward transforms
208 // } else if lhs_poly.rhs_timescale == t.time_scale
209 // && rhs_poly.lhs_timescale == target
210 // {
211 // // indirect backward transforms
212
213 // // |BDT-GST|=a0_bdt & |GST-GPST|=a1 dt_gst
214 // // BDT = a0_bdt + GST
215 // // GPST = GST -a1 dt_gpst
216 // // GPST-BDT (backward indirect) = -a1 -a0
217
218 // let dt_lhs_s = (t.to_time_scale(lhs_poly.lhs_timescale)
219 // - lhs_poly.ref_epoch)
220 // .to_seconds();
221
222 // let dt_rhs_s = (t.to_time_scale(rhs_poly.lhs_timescale)
223 // - rhs_poly.ref_epoch)
224 // .to_seconds();
225
226 // let correction_a = lhs_poly.polynomial.constant.to_seconds()
227 // + lhs_poly.polynomial.rate.to_seconds() * dt_lhs_s
228 // + lhs_poly.polynomial.accel.to_seconds() * dt_lhs_s.powi(2);
229
230 // // println!("correction = {}", correction_a);
231
232 // let correction_b = rhs_poly.polynomial.constant.to_seconds()
233 // + rhs_poly.polynomial.rate.to_seconds() * dt_rhs_s
234 // + rhs_poly.polynomial.accel.to_seconds() * dt_rhs_s.powi(2);
235
236 // // println!("correction = {}", correction_b);
237
238 // return Some(
239 // t.to_time_scale(target)
240 // + Duration::from_seconds(correction_a)
241 // + Duration::from_seconds(correction_b),
242 // );
243 // }
244 // }
245 // }
246
247 // None
248}
249
250impl Merge for TimeCorrectionsDB {
251 fn merge(&self, rhs: &Self) -> Result<Self, MergeError>
252 where
253 Self: Sized,
254 {
255 let mut s = self.clone();
256 s.merge_mut(rhs)?;
257
258 Ok(s)
259 }
260
261 fn merge_mut(&mut self, rhs: &Self) -> Result<(), MergeError> {
262 // latch new corrections
263 for polynomial in rhs.corrections.iter() {
264 self.corrections.push(*polynomial);
265 }
266 Ok(())
267 }
268}
269
270#[cfg(test)]
271mod test {
272 use crate::{TimeCorrection, TimeCorrectionsDB};
273 use hifitime::{Duration, Epoch, Polynomial, TimeScale};
274 use std::str::FromStr;
275
276 #[test]
277 fn time_corrections_db_without_strict_validity() {
278 let t_ref_gpst = Epoch::from_str("2020-01-01T00:00:00 GPST").unwrap();
279
280 let (a0, _, _) = (1.0E-9, 0.0, 0.0);
281
282 let polynomial = Polynomial {
283 constant: Duration::from_seconds(a0),
284 rate: Duration::ZERO,
285 accel: Duration::ZERO,
286 };
287
288 let mut database = TimeCorrectionsDB::default();
289
290 database.add(TimeCorrection {
291 lhs_timescale: TimeScale::GST,
292 rhs_timescale: TimeScale::GPST,
293 ref_epoch: t_ref_gpst,
294 polynomial,
295 validity_period: Duration::from_hours(1.0),
296 });
297
298 // Random date in GST
299 let t_gst = Epoch::from_str("2020-01-01T00:00:00 GST").unwrap();
300
301 let t_gst_gpst = database
302 .precise_epoch_correction(t_gst, TimeScale::GPST)
303 .unwrap();
304
305 assert_eq!(t_gst_gpst.time_scale, TimeScale::GPST);
306
307 // Random date in GPST
308 let t_gpst = Epoch::from_str("2020-01-01T00:00:10 GPST").unwrap();
309
310 let t_gpst_gst = database
311 .precise_epoch_correction(t_gpst, TimeScale::GST)
312 .unwrap();
313
314 assert_eq!(t_gpst_gst.time_scale, TimeScale::GST);
315
316 // Random date in UTC
317 let t_utc = Epoch::from_str("2020-01-01T00:00:10 UTC").unwrap();
318
319 assert!(database
320 .precise_epoch_correction(t_utc, TimeScale::GST)
321 .is_none());
322
323 assert!(database
324 .precise_epoch_correction(t_utc, TimeScale::GPST)
325 .is_none());
326 }
327
328 #[test]
329 #[ignore]
330 fn test_indirect_forward_transform_not_utc() {
331 let t_ref_bdt = Epoch::from_str("2020-01-01T00:00:00 BDT").unwrap();
332 let t_ref_gst = Epoch::from_str("2020-01-01T00:00:00 GST").unwrap();
333 //let t_ref_gpst = Epoch::from_str("2020-01-01T00:00:00 GPST").unwrap();
334
335 let (a0_bdt_gst, _, _) = (1.0E-9, 0.0, 0.0);
336 let (a0_gst_gpst, _, _) = (2.0E-9, 0.0, 0.0);
337
338 let mut solver = TimeCorrectionsDB::default();
339
340 solver.add(TimeCorrection {
341 lhs_timescale: TimeScale::BDT,
342 rhs_timescale: TimeScale::GST,
343 validity_period: Duration::from_hours(1.0),
344 ref_epoch: t_ref_bdt,
345 polynomial: Polynomial {
346 constant: Duration::from_seconds(a0_bdt_gst),
347 rate: Duration::ZERO,
348 accel: Duration::ZERO,
349 },
350 });
351
352 solver.add(TimeCorrection {
353 lhs_timescale: TimeScale::GST,
354 rhs_timescale: TimeScale::GPST,
355 validity_period: Duration::from_hours(1.0),
356 ref_epoch: t_ref_gst,
357 polynomial: Polynomial {
358 constant: Duration::from_seconds(a0_gst_gpst),
359 rate: Duration::ZERO,
360 accel: Duration::ZERO,
361 },
362 });
363
364 // verify direct transforms still work
365 let t_gst = Epoch::from_str("2020-01-01T00:00:00 GST").unwrap();
366 let t_bdt = Epoch::from_str("2020-01-01T00:00:00 BDT").unwrap();
367 let t_gpst = Epoch::from_str("2020-01-01T00:00:00 GPST").unwrap();
368
369 let t_gst_gpst = solver
370 .precise_epoch_correction(t_gst, TimeScale::GPST)
371 .unwrap();
372
373 assert_eq!(t_gst_gpst.time_scale, TimeScale::GPST);
374
375 let t_gpst_gst = solver
376 .precise_epoch_correction(t_gpst, TimeScale::GST)
377 .unwrap();
378
379 assert_eq!(t_gpst_gst.time_scale, TimeScale::GST);
380
381 let t_gst_bdt = solver
382 .precise_epoch_correction(t_gst, TimeScale::BDT)
383 .unwrap();
384
385 assert_eq!(t_gst_bdt.time_scale, TimeScale::BDT);
386
387 let t_bdt_gst = solver
388 .precise_epoch_correction(t_bdt, TimeScale::GST)
389 .unwrap();
390
391 assert_eq!(t_bdt_gst.time_scale, TimeScale::GST);
392
393 // indirect forward transform
394 let t_bdt_gpst = solver
395 .precise_epoch_correction(t_bdt, TimeScale::GPST)
396 .unwrap();
397
398 assert_eq!(t_bdt_gpst.time_scale, TimeScale::GPST);
399
400 let coarsed = t_bdt.to_time_scale(TimeScale::GPST);
401 let dt = coarsed - t_bdt_gpst;
402
403 assert_eq!(
404 dt,
405 Duration::from_seconds(a0_bdt_gst) + Duration::from_seconds(a0_gst_gpst)
406 );
407
408 // linearity
409 let reciprocal = solver
410 .precise_epoch_correction(t_bdt_gpst, TimeScale::BDT)
411 .unwrap();
412
413 assert_eq!(reciprocal.time_scale, TimeScale::BDT);
414 assert_eq!(reciprocal, t_bdt);
415
416 // indirect backward transform
417 let t_gpst_bdt = solver
418 .precise_epoch_correction(t_gpst, TimeScale::BDT)
419 .unwrap();
420
421 assert_eq!(t_gpst_bdt.time_scale, TimeScale::BDT);
422
423 let coarsed = t_gpst.to_time_scale(TimeScale::BDT);
424 let dt = coarsed - t_gpst_bdt;
425
426 assert_eq!(
427 dt,
428 Duration::from_seconds(a0_bdt_gst) + Duration::from_seconds(a0_gst_gpst)
429 );
430
431 // linearity
432 let reciprocal = solver
433 .precise_epoch_correction(t_gpst_bdt, TimeScale::GPST)
434 .unwrap();
435
436 assert_eq!(reciprocal.time_scale, TimeScale::GPST);
437 assert_eq!(reciprocal, t_gpst);
438 }
439
440 #[test]
441 #[ignore]
442 fn test_indirect_forward_transform_utc() {
443 let t_ref_bdt = Epoch::from_str("2020-01-01T00:00:00 BDT").unwrap();
444 //let t_ref_gst = Epoch::from_str("2020-01-01T00:00:00 GST").unwrap();
445 let t_ref_gpst = Epoch::from_str("2020-01-01T00:00:00 GPST").unwrap();
446
447 let (a0_bdt_gst, _, _) = (1.0E-9, 0.0, 0.0);
448 let (a0_gpst_utc, _, _) = (2.0E-9, 0.0, 0.0);
449
450 let mut database = TimeCorrectionsDB::default();
451
452 database.add(TimeCorrection {
453 lhs_timescale: TimeScale::BDT,
454 rhs_timescale: TimeScale::GST,
455 ref_epoch: t_ref_bdt,
456 polynomial: Polynomial {
457 constant: Duration::from_seconds(a0_bdt_gst),
458 rate: Duration::ZERO,
459 accel: Duration::ZERO,
460 },
461 validity_period: Duration::from_hours(1.0),
462 });
463
464 database.add(TimeCorrection {
465 lhs_timescale: TimeScale::GPST,
466 rhs_timescale: TimeScale::UTC,
467 ref_epoch: t_ref_gpst,
468 polynomial: Polynomial {
469 constant: Duration::from_seconds(a0_gpst_utc),
470 rate: Duration::ZERO,
471 accel: Duration::ZERO,
472 },
473 validity_period: Duration::from_hours(1.0),
474 });
475
476 // verify direct transforms still work
477 let t_gst = Epoch::from_str("2020-01-01T00:00:00 GST").unwrap();
478 let t_bdt = Epoch::from_str("2020-01-01T00:00:00 BDT").unwrap();
479 let t_gpst = Epoch::from_str("2020-01-01T00:00:00 GPST").unwrap();
480
481 let t_gpst_utc = database
482 .precise_epoch_correction(t_gpst, TimeScale::UTC)
483 .unwrap();
484
485 assert_eq!(t_gpst_utc.time_scale, TimeScale::UTC);
486
487 // linearity
488 let reciprocal = database
489 .precise_epoch_correction(t_gpst_utc, TimeScale::GPST)
490 .unwrap();
491
492 assert_eq!(reciprocal.time_scale, TimeScale::GPST);
493 assert_eq!(reciprocal, t_gpst);
494
495 let t_gst_bdt = database
496 .precise_epoch_correction(t_gst, TimeScale::BDT)
497 .unwrap();
498
499 assert_eq!(t_gst_bdt.time_scale, TimeScale::BDT);
500
501 let t_bdt_gst = database
502 .precise_epoch_correction(t_bdt, TimeScale::GST)
503 .unwrap();
504
505 assert_eq!(t_bdt_gst.time_scale, TimeScale::GST);
506
507 // indirect forward transform
508 let t_bdt_utc = database
509 .precise_epoch_correction(t_bdt, TimeScale::UTC)
510 .unwrap();
511
512 assert_eq!(t_bdt_utc.time_scale, TimeScale::UTC);
513
514 let coarsed = t_bdt.to_time_scale(TimeScale::UTC);
515 let dt = coarsed - t_bdt_utc;
516
517 assert_eq!(
518 dt,
519 Duration::from_seconds(a0_bdt_gst) + Duration::from_seconds(a0_gpst_utc)
520 );
521
522 // linearity
523 let reciprocal = database
524 .precise_epoch_correction(t_bdt_utc, TimeScale::BDT)
525 .unwrap();
526
527 assert_eq!(reciprocal.time_scale, TimeScale::BDT);
528 assert_eq!(reciprocal, t_bdt);
529 }
530}