Skip to main content

spatialrust_sync/
clock.rs

1//! Clock domains and sync quality attached to observation times.
2
3use spatialrust_core::Timestamp;
4
5/// Named clock source (host, sensor board, ROS time, ...).
6#[derive(Clone, Debug, PartialEq, Eq, Hash)]
7pub struct ClockId(pub String);
8
9impl ClockId {
10    /// Creates a clock identifier.
11    #[must_use]
12    pub fn new(value: impl Into<String>) -> Self {
13        Self(value.into())
14    }
15
16    /// Borrows the identifier.
17    #[must_use]
18    pub fn as_str(&self) -> &str {
19        &self.0
20    }
21}
22
23impl From<&str> for ClockId {
24    fn from(value: &str) -> Self {
25        Self(value.to_owned())
26    }
27}
28
29/// Semantic class of a clock domain.
30#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
31pub enum ClockDomain {
32    /// Host monotonic / steady clock.
33    #[default]
34    HostSteady,
35    /// Host wall / UTC-aligned clock.
36    HostWall,
37    /// Sensor-local free-running clock.
38    Sensor,
39    /// Externally synchronized domain (PTP/NTP/ROS `/clock`).
40    External,
41}
42
43/// Quality of time synchronization relative to a reference clock.
44#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
45pub struct SyncQuality {
46    /// Estimated offset from the reference clock in nanoseconds.
47    pub offset_ns: i64,
48    /// Estimated 1-sigma uncertainty in nanoseconds.
49    pub uncertainty_ns: u64,
50    /// Whether the offset is an estimate rather than a hard measurement.
51    pub estimated: bool,
52}
53
54impl SyncQuality {
55    /// Exact sync with zero offset/uncertainty.
56    #[must_use]
57    pub const fn exact() -> Self {
58        Self { offset_ns: 0, uncertainty_ns: 0, estimated: false }
59    }
60
61    /// Returns whether this stamp is usable for tight multimodal fusion.
62    #[must_use]
63    pub const fn is_tight(self, max_uncertainty_ns: u64) -> bool {
64        self.uncertainty_ns <= max_uncertainty_ns
65    }
66}
67
68/// Timestamp anchored to an explicit clock domain and sync quality.
69#[derive(Clone, Debug, PartialEq, Eq)]
70pub struct StampedTime {
71    /// Clock that produced the timestamp.
72    pub clock: ClockId,
73    /// Domain class for the clock.
74    pub domain: ClockDomain,
75    /// Raw timestamp on `clock`.
76    pub timestamp: Timestamp,
77    /// Sync quality versus an episode reference clock.
78    pub quality: SyncQuality,
79}
80
81impl StampedTime {
82    /// Creates a stamped time with exact sync quality.
83    #[must_use]
84    pub fn exact(clock: impl Into<ClockId>, domain: ClockDomain, timestamp: Timestamp) -> Self {
85        Self { clock: clock.into(), domain, timestamp, quality: SyncQuality::exact() }
86    }
87
88    /// Returns nanoseconds on the owning clock.
89    #[must_use]
90    pub const fn as_nanos(&self) -> u64 {
91        self.timestamp.as_nanos()
92    }
93
94    /// Absolute nanosecond distance to another stamp (ignores clock identity).
95    #[must_use]
96    pub fn abs_delta_ns(&self, other: &Self) -> u64 {
97        let a = self.as_nanos();
98        let b = other.as_nanos();
99        a.abs_diff(b)
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::{ClockDomain, StampedTime, SyncQuality};
106    use spatialrust_core::Timestamp;
107
108    #[test]
109    fn tight_sync_rejects_high_uncertainty() {
110        let quality = SyncQuality { offset_ns: 0, uncertainty_ns: 5_000_000, estimated: true };
111        assert!(!quality.is_tight(1_000_000));
112        let stamp = StampedTime::exact("host", ClockDomain::HostSteady, Timestamp::from_nanos(10));
113        assert_eq!(stamp.as_nanos(), 10);
114    }
115}