Skip to main content

elara_core/
time.rs

1//! Time primitives for ELARA protocol
2//!
3//! ELARA uses a dual-clock system:
4//! - τp (Perceptual Time): monotonic, smooth, local-driven
5//! - τs (State Time): elastic, drift-correctable, convergence-oriented
6
7use std::ops::{Add, Sub};
8use std::time::Duration;
9
10/// State time (τs) - elastic, convergence-oriented
11/// Represented as microseconds since session epoch
12#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
13pub struct StateTime(pub i64);
14
15impl StateTime {
16    pub const ZERO: StateTime = StateTime(0);
17    pub const MAX: StateTime = StateTime(i64::MAX);
18    pub const MIN: StateTime = StateTime(i64::MIN);
19
20    #[inline]
21    pub fn from_micros(micros: i64) -> Self {
22        StateTime(micros)
23    }
24
25    #[inline]
26    pub fn from_millis(millis: i64) -> Self {
27        StateTime(millis * 1000)
28    }
29
30    #[inline]
31    pub fn from_secs_f64(secs: f64) -> Self {
32        StateTime((secs * 1_000_000.0) as i64)
33    }
34
35    #[inline]
36    pub fn as_micros(self) -> i64 {
37        self.0
38    }
39
40    #[inline]
41    pub fn as_millis(self) -> i64 {
42        self.0 / 1000
43    }
44
45    #[inline]
46    pub fn as_secs_f64(self) -> f64 {
47        self.0 as f64 / 1_000_000.0
48    }
49
50    /// Convert to wire format (100μs units, 32-bit offset)
51    #[inline]
52    pub fn to_wire_offset(self, reference: StateTime) -> i32 {
53        let diff_100us = (self.0 - reference.0) / 100;
54        diff_100us.clamp(i32::MIN as i64, i32::MAX as i64) as i32
55    }
56
57    /// Convert from wire format (100μs units, 32-bit offset)
58    #[inline]
59    pub fn from_wire_offset(reference: StateTime, offset_100us: i32) -> Self {
60        StateTime(reference.0 + (offset_100us as i64 * 100))
61    }
62
63    #[inline]
64    pub fn saturating_add(self, duration: Duration) -> Self {
65        StateTime(self.0.saturating_add(duration.as_micros() as i64))
66    }
67
68    #[inline]
69    pub fn saturating_sub(self, duration: Duration) -> Self {
70        StateTime(self.0.saturating_sub(duration.as_micros() as i64))
71    }
72}
73
74impl Add<Duration> for StateTime {
75    type Output = StateTime;
76
77    #[inline]
78    fn add(self, rhs: Duration) -> Self::Output {
79        StateTime(self.0 + rhs.as_micros() as i64)
80    }
81}
82
83impl Sub<Duration> for StateTime {
84    type Output = StateTime;
85
86    #[inline]
87    fn sub(self, rhs: Duration) -> Self::Output {
88        StateTime(self.0 - rhs.as_micros() as i64)
89    }
90}
91
92impl Sub<StateTime> for StateTime {
93    type Output = Duration;
94
95    #[inline]
96    fn sub(self, rhs: StateTime) -> Self::Output {
97        let diff = self.0 - rhs.0;
98        if diff >= 0 {
99            Duration::from_micros(diff as u64)
100        } else {
101            Duration::ZERO
102        }
103    }
104}
105
106impl std::fmt::Debug for StateTime {
107    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108        write!(f, "τs({:.3}ms)", self.as_millis() as f64)
109    }
110}
111
112/// Perceptual time (τp) - monotonic, smooth, local-driven
113/// Represented as microseconds since node start
114#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
115pub struct PerceptualTime(pub u64);
116
117impl PerceptualTime {
118    pub const ZERO: PerceptualTime = PerceptualTime(0);
119
120    #[inline]
121    pub fn from_micros(micros: u64) -> Self {
122        PerceptualTime(micros)
123    }
124
125    #[inline]
126    pub fn from_millis(millis: u64) -> Self {
127        PerceptualTime(millis * 1000)
128    }
129
130    #[inline]
131    pub fn from_secs_f64(secs: f64) -> Self {
132        PerceptualTime((secs * 1_000_000.0) as u64)
133    }
134
135    #[inline]
136    pub fn as_micros(self) -> u64 {
137        self.0
138    }
139
140    #[inline]
141    pub fn as_millis(self) -> u64 {
142        self.0 / 1000
143    }
144
145    #[inline]
146    pub fn as_secs_f64(self) -> f64 {
147        self.0 as f64 / 1_000_000.0
148    }
149
150    #[inline]
151    pub fn saturating_add(self, duration: Duration) -> Self {
152        PerceptualTime(self.0.saturating_add(duration.as_micros() as u64))
153    }
154}
155
156impl Add<Duration> for PerceptualTime {
157    type Output = PerceptualTime;
158
159    #[inline]
160    fn add(self, rhs: Duration) -> Self::Output {
161        PerceptualTime(self.0 + rhs.as_micros() as u64)
162    }
163}
164
165impl Sub<PerceptualTime> for PerceptualTime {
166    type Output = Duration;
167
168    #[inline]
169    fn sub(self, rhs: PerceptualTime) -> Self::Output {
170        Duration::from_micros(self.0.saturating_sub(rhs.0))
171    }
172}
173
174impl std::fmt::Debug for PerceptualTime {
175    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176        write!(f, "τp({:.3}ms)", self.as_millis() as f64)
177    }
178}
179
180/// Reality Window - defines the temporal bounds for event processing
181/// RW = [τs - Hc, τs + Hp]
182#[derive(Clone, Copy, Debug)]
183pub struct RealityWindow {
184    /// Current state time
185    pub τs: StateTime,
186    /// Correction horizon (how far back we can correct)
187    pub hc: Duration,
188    /// Prediction horizon (how far ahead we predict)
189    pub hp: Duration,
190}
191
192impl RealityWindow {
193    pub fn new(τs: StateTime, hc: Duration, hp: Duration) -> Self {
194        RealityWindow { τs, hc, hp }
195    }
196
197    /// Left bound of reality window (oldest correctable time)
198    #[inline]
199    pub fn left(&self) -> StateTime {
200        self.τs.saturating_sub(self.hc)
201    }
202
203    /// Right bound of reality window (furthest predicted time)
204    #[inline]
205    pub fn right(&self) -> StateTime {
206        self.τs.saturating_add(self.hp)
207    }
208
209    /// Check if a time is within the reality window
210    #[inline]
211    pub fn contains(&self, t: StateTime) -> bool {
212        t >= self.left() && t <= self.right()
213    }
214
215    /// Classify a time relative to the reality window
216    pub fn classify(&self, t: StateTime) -> TimePosition {
217        if t < self.left() {
218            TimePosition::TooLate
219        } else if t < self.τs {
220            TimePosition::Correctable
221        } else if t <= self.τs + Duration::from_millis(5) {
222            TimePosition::Current
223        } else if t <= self.right() {
224            TimePosition::Predictable
225        } else {
226            TimePosition::TooEarly
227        }
228    }
229}
230
231/// Position of a time relative to the reality window
232#[derive(Clone, Copy, Debug, PartialEq, Eq)]
233pub enum TimePosition {
234    /// Before correction horizon - too late to process
235    TooLate,
236    /// Within correction horizon - can be blended in
237    Correctable,
238    /// At current time - apply directly
239    Current,
240    /// Within prediction horizon - replace prediction
241    Predictable,
242    /// Beyond prediction horizon - buffer for later
243    TooEarly,
244}
245
246/// Time intent carried by events
247#[derive(Clone, Copy, Debug, Default)]
248pub struct TimeIntent {
249    /// Intended state time (relative offset in wire format)
250    pub τs_offset: i32,
251    /// Optional deadline for perceptual events
252    pub deadline: Option<i32>,
253}
254
255impl TimeIntent {
256    pub fn new(τs_offset: i32) -> Self {
257        TimeIntent {
258            τs_offset,
259            deadline: None,
260        }
261    }
262
263    pub fn ts_offset(&self) -> i32 {
264        self.τs_offset
265    }
266
267    pub fn with_deadline(mut self, deadline: i32) -> Self {
268        self.deadline = Some(deadline);
269        self
270    }
271
272    /// Convert to absolute state time given a reference
273    pub fn to_absolute(&self, reference: StateTime) -> StateTime {
274        StateTime::from_wire_offset(reference, self.τs_offset)
275    }
276}
277
278#[cfg(test)]
279mod tests {
280    use super::*;
281
282    #[test]
283    fn test_state_time_wire_roundtrip() {
284        let reference = StateTime::from_millis(1000);
285        let time = StateTime::from_millis(1050);
286
287        let offset = time.to_wire_offset(reference);
288        let recovered = StateTime::from_wire_offset(reference, offset);
289
290        // Should be within 100μs precision
291        assert!((time.0 - recovered.0).abs() < 100);
292    }
293
294    #[test]
295    fn test_reality_window_classification() {
296        let τs = StateTime::from_millis(1000);
297        let hc = Duration::from_millis(100);
298        let hp = Duration::from_millis(50);
299        let rw = RealityWindow::new(τs, hc, hp);
300
301        // Too late (before Hc)
302        assert_eq!(
303            rw.classify(StateTime::from_millis(850)),
304            TimePosition::TooLate
305        );
306
307        // Correctable (within Hc)
308        assert_eq!(
309            rw.classify(StateTime::from_millis(950)),
310            TimePosition::Correctable
311        );
312
313        // Current (at τs)
314        assert_eq!(
315            rw.classify(StateTime::from_millis(1000)),
316            TimePosition::Current
317        );
318
319        // Predictable (within Hp)
320        assert_eq!(
321            rw.classify(StateTime::from_millis(1030)),
322            TimePosition::Predictable
323        );
324
325        // Too early (beyond Hp)
326        assert_eq!(
327            rw.classify(StateTime::from_millis(1100)),
328            TimePosition::TooEarly
329        );
330    }
331
332    #[test]
333    fn test_perceptual_time_monotonic() {
334        let t1 = PerceptualTime::from_millis(100);
335        let t2 = t1 + Duration::from_millis(10);
336
337        assert!(t2 > t1);
338        assert_eq!(t2 - t1, Duration::from_millis(10));
339    }
340}