Skip to main content

elara_time/
clock.rs

1//! Clock implementations for ELARA Time Engine
2
3use std::time::{Duration, Instant};
4
5use elara_core::{PerceptualTime, StateTime};
6
7/// Perceptual clock (τp) - monotonic, smooth, local-driven
8/// INVARIANT: τp MUST be monotonically increasing, NEVER jumps
9pub struct PerceptualClock {
10    /// Current perceptual time
11    value: PerceptualTime,
12    /// Last update instant
13    last_update: Instant,
14}
15
16const MAX_PERCEPTUAL_TICK: Duration = Duration::from_millis(100);
17
18impl PerceptualClock {
19    /// Create a new perceptual clock starting at zero
20    pub fn new() -> Self {
21        let now = Instant::now();
22        PerceptualClock {
23            value: PerceptualTime::ZERO,
24            last_update: now,
25        }
26    }
27
28    /// Advance the clock based on elapsed real time
29    /// Returns the new perceptual time
30    pub fn tick(&mut self) -> PerceptualTime {
31        let now = Instant::now();
32        let elapsed = now.duration_since(self.last_update);
33
34        let clamped = if elapsed > MAX_PERCEPTUAL_TICK {
35            MAX_PERCEPTUAL_TICK
36        } else {
37            elapsed
38        };
39
40        self.value = self.value.saturating_add(clamped);
41        self.last_update = now;
42        self.value
43    }
44
45    /// Get current perceptual time without advancing
46    pub fn now(&self) -> PerceptualTime {
47        self.value
48    }
49
50    /// Check if clock is advancing (sanity check)
51    pub fn is_advancing(&self) -> bool {
52        // Clock is always advancing as long as tick() is called
53        true
54    }
55}
56
57impl Default for PerceptualClock {
58    fn default() -> Self {
59        Self::new()
60    }
61}
62
63/// State clock (τs) - elastic, drift-correctable, convergence-oriented
64/// CAN be bent for convergence, but maintains causality
65pub struct StateClock {
66    /// Current state time
67    value: StateTime,
68    /// Clock rate multiplier (1.0 = real-time)
69    rate: f64,
70    /// Convergence target (if any)
71    convergence_target: Option<StateTime>,
72    /// Maximum correction per tick
73    max_correction_per_tick: Duration,
74}
75
76impl StateClock {
77    /// Create a new state clock starting at zero
78    pub fn new() -> Self {
79        StateClock {
80            value: StateTime::ZERO,
81            rate: 1.0,
82            convergence_target: None,
83            max_correction_per_tick: Duration::from_millis(10),
84        }
85    }
86
87    /// Advance the clock by a duration, applying rate and convergence
88    /// Returns the new state time
89    pub fn advance(&mut self, dt: Duration) -> StateTime {
90        // Base advance with rate
91        let base_advance_us = (dt.as_micros() as f64 * self.rate) as i64;
92
93        // Apply convergence correction if needed
94        let correction = if let Some(target) = self.convergence_target {
95            let error = target.as_micros() - self.value.as_micros();
96            let max_correction = self.max_correction_per_tick.as_micros() as i64;
97
98            // Proportional correction (10% of error, clamped)
99            let correction = (error as f64 * 0.1) as i64;
100            correction.clamp(-max_correction, max_correction)
101        } else {
102            0
103        };
104
105        self.value = StateTime::from_micros(self.value.as_micros() + base_advance_us + correction);
106        self.value
107    }
108
109    /// Get current state time
110    pub fn now(&self) -> StateTime {
111        self.value
112    }
113
114    /// Set convergence target
115    pub fn set_convergence_target(&mut self, target: StateTime) {
116        self.convergence_target = Some(target);
117    }
118
119    /// Clear convergence target
120    pub fn clear_convergence_target(&mut self) {
121        self.convergence_target = None;
122    }
123
124    /// Set clock rate (for time dilation)
125    /// Rate must be between 0.5 and 2.0
126    pub fn set_rate(&mut self, rate: f64) {
127        self.rate = rate.clamp(0.5, 2.0);
128    }
129
130    /// Get current rate
131    pub fn rate(&self) -> f64 {
132        self.rate
133    }
134
135    /// Sync to a specific time (for recovery)
136    /// Only allowed to move forward
137    pub fn sync_to(&mut self, target: StateTime) {
138        if target > self.value {
139            self.value = target;
140        }
141    }
142}
143
144impl Default for StateClock {
145    fn default() -> Self {
146        Self::new()
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn test_perceptual_clock_monotonic() {
156        let mut clock = PerceptualClock::new();
157
158        let t1 = clock.tick();
159        std::thread::sleep(Duration::from_millis(10));
160        let t2 = clock.tick();
161
162        assert!(t2 > t1);
163    }
164
165    #[test]
166    fn test_state_clock_advance() {
167        let mut clock = StateClock::new();
168
169        let t1 = clock.now();
170        clock.advance(Duration::from_millis(100));
171        let t2 = clock.now();
172
173        assert!(t2 > t1);
174        // Should be approximately 100ms later
175        let diff = t2.as_micros() - t1.as_micros();
176        assert!((99_000..=101_000).contains(&diff));
177    }
178
179    #[test]
180    fn test_state_clock_rate() {
181        let mut clock = StateClock::new();
182
183        // Double speed
184        clock.set_rate(2.0);
185        clock.advance(Duration::from_millis(100));
186
187        // Should advance ~200ms
188        let value = clock.now().as_micros();
189        assert!((190_000..=210_000).contains(&value));
190    }
191
192    #[test]
193    fn test_state_clock_convergence() {
194        let mut clock = StateClock::new();
195
196        // Set target ahead
197        clock.set_convergence_target(StateTime::from_millis(1000));
198
199        // Advance multiple times
200        for _ in 0..100 {
201            clock.advance(Duration::from_millis(10));
202        }
203
204        // Should be closer to target than just 1000ms of advance
205        let value = clock.now().as_millis();
206        assert!(value > 1000); // Converged toward target
207    }
208}