Skip to main content

elara_time/
engine.rs

1//! Time Engine - orchestrates dual clocks, horizons, and temporal control
2
3use std::time::Duration;
4
5use elara_core::{NodeId, PerceptualTime, RealityWindow, StateTime, TimePosition};
6
7use crate::{NetworkModel, PerceptualClock, StateClock};
8
9/// Time Engine configuration
10#[derive(Clone, Debug)]
11pub struct TimeEngineConfig {
12    /// Minimum prediction horizon
13    pub Hp_min: Duration,
14    /// Maximum prediction horizon
15    pub Hp_max: Duration,
16    /// Minimum correction horizon
17    pub Hc_min: Duration,
18    /// Maximum correction horizon
19    pub Hc_max: Duration,
20    /// Jitter sensitivity for Hp
21    pub k1_jitter: f64,
22    /// Reorder sensitivity for Hp
23    pub k2_reorder: f64,
24    /// Loss sensitivity for Hp
25    pub k3_loss: f64,
26    /// Jitter sensitivity for Hc
27    pub k4_jitter_correct: f64,
28    /// Tick interval
29    pub tick_interval: Duration,
30}
31
32impl Default for TimeEngineConfig {
33    fn default() -> Self {
34        // MSP defaults
35        TimeEngineConfig {
36            Hp_min: Duration::from_millis(40),
37            Hp_max: Duration::from_millis(300),
38            Hc_min: Duration::from_millis(80),
39            Hc_max: Duration::from_millis(600),
40            k1_jitter: 2.5,
41            k2_reorder: 15.0,
42            k3_loss: 150.0,
43            k4_jitter_correct: 2.0,
44            tick_interval: Duration::from_millis(10),
45        }
46    }
47}
48
49impl TimeEngineConfig {
50    /// Configuration for low-bandwidth networks (2G)
51    pub fn low_bandwidth() -> Self {
52        TimeEngineConfig {
53            Hp_min: Duration::from_millis(80),
54            Hp_max: Duration::from_millis(500),
55            Hc_min: Duration::from_millis(150),
56            Hc_max: Duration::from_millis(1000),
57            k1_jitter: 3.0,
58            k2_reorder: 20.0,
59            k3_loss: 200.0,
60            k4_jitter_correct: 2.5,
61            tick_interval: Duration::from_millis(15),
62        }
63    }
64}
65
66/// Time Engine - manages dual clocks and temporal control
67pub struct TimeEngine {
68    /// Perceptual clock (τp)
69    perceptual: PerceptualClock,
70    /// State clock (τs)
71    state: StateClock,
72    /// Network model
73    network: NetworkModel,
74    /// Current prediction horizon
75    Hp: Duration,
76    /// Current correction horizon
77    Hc: Duration,
78    /// Configuration
79    config: TimeEngineConfig,
80}
81
82impl TimeEngine {
83    /// Create a new Time Engine with default configuration
84    pub fn new() -> Self {
85        Self::with_config(TimeEngineConfig::default())
86    }
87
88    /// Create a new Time Engine with custom configuration
89    pub fn with_config(config: TimeEngineConfig) -> Self {
90        TimeEngine {
91            perceptual: PerceptualClock::new(),
92            state: StateClock::new(),
93            network: NetworkModel::new(),
94            Hp: config.Hp_min,
95            Hc: config.Hc_min,
96            config,
97        }
98    }
99
100    /// Advance clocks by one tick
101    /// This is the core tick function - MUST be called every tick
102    pub fn tick(&mut self) {
103        // τp ALWAYS advances - this is the never-freeze guarantee
104        self.perceptual.tick();
105
106        // τs advances with potential convergence correction
107        self.state.advance(self.config.tick_interval);
108
109        // Adjust horizons based on network quality
110        self.adjust_horizons();
111    }
112
113    /// Get current perceptual time (τp)
114    pub fn τp(&self) -> PerceptualTime {
115        self.perceptual.now()
116    }
117
118    pub fn tau_p(&self) -> PerceptualTime {
119        self.perceptual.now()
120    }
121
122    /// Get current state time (τs)
123    pub fn τs(&self) -> StateTime {
124        self.state.now()
125    }
126
127    pub fn tau_s(&self) -> StateTime {
128        self.state.now()
129    }
130
131    /// Get current prediction horizon
132    pub fn Hp(&self) -> Duration {
133        self.Hp
134    }
135
136    /// Get current correction horizon
137    pub fn Hc(&self) -> Duration {
138        self.Hc
139    }
140
141    /// Get current reality window
142    pub fn reality_window(&self) -> RealityWindow {
143        RealityWindow::new(self.state.now(), self.Hc, self.Hp)
144    }
145
146    /// Classify a time relative to the reality window
147    pub fn classify_time(&self, t: StateTime) -> TimePosition {
148        self.reality_window().classify(t)
149    }
150
151    /// Update network model from a received packet
152    pub fn update_from_packet(&mut self, peer: NodeId, remote_time: StateTime, seq: u16) {
153        let local_time = self.state.now().as_secs_f64();
154        let remote_time_f = remote_time.as_secs_f64();
155        self.network
156            .update_from_packet(peer, local_time, remote_time_f, seq);
157    }
158
159    /// Record packet reorder
160    pub fn record_reorder(&mut self, depth: u32) {
161        self.network.record_reorder(depth);
162    }
163
164    /// Record packet loss
165    pub fn record_loss(&mut self, lost: u32, total: u32) {
166        self.network.record_loss(lost, total);
167    }
168
169    /// Get network stability score
170    pub fn stability_score(&self) -> f64 {
171        self.network.stability_score
172    }
173
174    /// Adjust horizons based on network quality
175    fn adjust_horizons(&mut self) {
176        let net = &self.network;
177        let cfg = &self.config;
178
179        // Prediction horizon: expands with network degradation
180        let Hp_raw = cfg.Hp_min.as_secs_f64()
181            + cfg.k1_jitter * net.jitter
182            + cfg.k2_reorder * net.reorder_depth as f64 * 0.001
183            + cfg.k3_loss * net.loss_rate;
184
185        self.Hp = Duration::from_secs_f64(
186            Hp_raw.clamp(cfg.Hp_min.as_secs_f64(), cfg.Hp_max.as_secs_f64()),
187        );
188
189        // Correction horizon: expands with jitter
190        let Hc_raw = cfg.Hc_min.as_secs_f64() + cfg.k4_jitter_correct * net.jitter;
191
192        self.Hc = Duration::from_secs_f64(
193            Hc_raw.clamp(cfg.Hc_min.as_secs_f64(), cfg.Hc_max.as_secs_f64()),
194        );
195    }
196
197    /// Calculate correction weight for a late event
198    /// Weight decreases as event gets older
199    pub fn correction_weight(&self, delay: Duration) -> f64 {
200        let Hc = self.Hc.as_secs_f64();
201        let delay_secs = delay.as_secs_f64();
202        (1.0 - delay_secs / Hc).clamp(0.0, 1.0)
203    }
204
205    /// Get reference to network model
206    pub fn network(&self) -> &NetworkModel {
207        &self.network
208    }
209
210    /// Get mutable reference to state clock (for convergence control)
211    pub fn state_clock_mut(&mut self) -> &mut StateClock {
212        &mut self.state
213    }
214}
215
216impl Default for TimeEngine {
217    fn default() -> Self {
218        Self::new()
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225
226    #[test]
227    fn test_time_engine_tick() {
228        let mut engine = TimeEngine::new();
229
230        let τp1 = engine.tau_p();
231        let τs1 = engine.tau_s();
232
233        engine.tick();
234
235        let τp2 = engine.tau_p();
236        let τs2 = engine.tau_s();
237
238        // Both clocks should advance
239        assert!(τp2 >= τp1);
240        assert!(τs2 >= τs1);
241    }
242
243    #[test]
244    fn test_reality_window() {
245        let engine = TimeEngine::new();
246        let rw = engine.reality_window();
247
248        // Window should be centered around τs
249        assert!(rw.left() < rw.τs);
250        assert!(rw.right() > rw.τs);
251    }
252
253    #[test]
254    fn test_horizon_adaptation() {
255        let mut engine = TimeEngine::new();
256
257        let initial_Hp = engine.Hp();
258
259        // Simulate bad network
260        engine.network.jitter = 0.1; // 100ms jitter
261        engine.network.loss_rate = 0.1; // 10% loss
262        engine.adjust_horizons();
263
264        // Hp should increase
265        assert!(engine.Hp() > initial_Hp);
266    }
267
268    #[test]
269    fn test_correction_weight() {
270        let engine = TimeEngine::new();
271
272        // No delay = full weight
273        assert!((engine.correction_weight(Duration::ZERO) - 1.0).abs() < 0.01);
274
275        // At Hc = zero weight
276        assert!(engine.correction_weight(engine.Hc()) < 0.01);
277
278        // Half Hc = half weight
279        let half_Hc = engine.Hc() / 2;
280        let weight = engine.correction_weight(half_Hc);
281        assert!((weight - 0.5).abs() < 0.1);
282    }
283}