Skip to main content

elara_test/
time_simulator.rs

1//! Time Engine Simulator - Full simulation harness for temporal testing
2//!
3//! Simulates:
4//! - Multiple nodes with independent clocks
5//! - Clock drift and skew
6//! - Network-induced timing variations
7//! - Reality window behavior under stress
8
9use std::collections::HashMap;
10use std::time::Duration;
11
12use elara_core::{NodeId, PerceptualTime, RealityWindow, StateTime, TimePosition};
13use elara_time::{TimeEngine, TimeEngineConfig};
14use rand::rngs::StdRng;
15use rand::{Rng, SeedableRng};
16
17use crate::chaos::{ChaosConfig, ChaosNetwork};
18
19/// Clock drift model for a simulated node
20#[derive(Clone, Debug)]
21pub struct ClockDriftModel {
22    /// Drift rate (1.0 = perfect, >1.0 = fast, <1.0 = slow)
23    pub drift_rate: f64,
24    /// Random jitter per tick (microseconds)
25    pub jitter_us: u32,
26    /// Accumulated drift
27    accumulated_drift: i64,
28}
29
30impl ClockDriftModel {
31    pub fn new(drift_rate: f64, jitter_us: u32) -> Self {
32        ClockDriftModel {
33            drift_rate,
34            jitter_us,
35            accumulated_drift: 0,
36        }
37    }
38
39    /// Perfect clock (no drift)
40    pub fn perfect() -> Self {
41        Self::new(1.0, 0)
42    }
43
44    /// Slightly fast clock
45    pub fn fast() -> Self {
46        Self::new(1.0001, 50)
47    }
48
49    /// Slightly slow clock
50    pub fn slow() -> Self {
51        Self::new(0.9999, 50)
52    }
53
54    /// Unstable clock with high jitter
55    pub fn unstable() -> Self {
56        Self::new(1.0, 500)
57    }
58
59    /// Apply drift to a duration
60    pub fn apply(&mut self, dt: Duration, rng: &mut StdRng) -> Duration {
61        let base_us = dt.as_micros() as f64;
62        let drifted_us = base_us * self.drift_rate;
63        let jitter = if self.jitter_us > 0 {
64            rng.gen_range(-(self.jitter_us as i32)..=self.jitter_us as i32) as f64
65        } else {
66            0.0
67        };
68        let final_us = (drifted_us + jitter).max(0.0) as u64;
69        self.accumulated_drift += final_us as i64 - dt.as_micros() as i64;
70        Duration::from_micros(final_us)
71    }
72
73    /// Get accumulated drift
74    pub fn accumulated_drift(&self) -> Duration {
75        Duration::from_micros(self.accumulated_drift.unsigned_abs())
76    }
77}
78
79/// Simulated node with time engine
80pub struct SimulatedNode {
81    /// Node ID
82    pub node_id: NodeId,
83    /// Time engine
84    pub time_engine: TimeEngine,
85    /// Clock drift model
86    pub drift_model: ClockDriftModel,
87    /// Local RNG
88    rng: StdRng,
89    /// Tick count
90    tick_count: u64,
91}
92
93impl SimulatedNode {
94    pub fn new(
95        node_id: NodeId,
96        config: TimeEngineConfig,
97        drift: ClockDriftModel,
98        seed: u64,
99    ) -> Self {
100        SimulatedNode {
101            node_id,
102            time_engine: TimeEngine::with_config(config),
103            drift_model: drift,
104            rng: StdRng::seed_from_u64(seed),
105            tick_count: 0,
106        }
107    }
108
109    /// Advance the node's time by one tick
110    pub fn tick(&mut self, real_dt: Duration) {
111        // Apply drift to get local perception of time
112        let _local_dt = self.drift_model.apply(real_dt, &mut self.rng);
113
114        // Advance time engine (it uses its own internal tick)
115        self.time_engine.tick();
116        self.tick_count += 1;
117    }
118
119    /// Get current perceptual time
120    pub fn tau_p(&self) -> PerceptualTime {
121        self.time_engine.tau_p()
122    }
123
124    /// Get current state time
125    pub fn tau_s(&self) -> StateTime {
126        self.time_engine.tau_s()
127    }
128
129    /// Get reality window
130    pub fn reality_window(&self) -> RealityWindow {
131        self.time_engine.reality_window()
132    }
133
134    /// Classify a time position
135    pub fn classify(&self, t: StateTime) -> TimePosition {
136        self.time_engine.classify_time(t)
137    }
138
139    /// Get tick count
140    pub fn tick_count(&self) -> u64 {
141        self.tick_count
142    }
143}
144
145/// Time simulation scenario
146pub struct TimeSimulator {
147    /// Simulated nodes
148    nodes: HashMap<NodeId, SimulatedNode>,
149    /// Network links between nodes
150    networks: HashMap<(NodeId, NodeId), ChaosNetwork>,
151    /// Global simulation time
152    global_time: Duration,
153    /// Tick interval
154    tick_interval: Duration,
155    /// RNG seed counter
156    seed_counter: u64,
157}
158
159impl TimeSimulator {
160    /// Create a new time simulator
161    pub fn new(tick_interval: Duration) -> Self {
162        TimeSimulator {
163            nodes: HashMap::new(),
164            networks: HashMap::new(),
165            global_time: Duration::ZERO,
166            tick_interval,
167            seed_counter: 0,
168        }
169    }
170
171    /// Add a node with default configuration
172    pub fn add_node(&mut self, node_id: NodeId) {
173        self.add_node_with_drift(node_id, ClockDriftModel::perfect());
174    }
175
176    /// Add a node with specific drift model
177    pub fn add_node_with_drift(&mut self, node_id: NodeId, drift: ClockDriftModel) {
178        let seed = self.seed_counter;
179        self.seed_counter += 1;
180
181        let node = SimulatedNode::new(node_id, TimeEngineConfig::default(), drift, seed);
182        self.nodes.insert(node_id, node);
183    }
184
185    /// Set network conditions between two nodes
186    pub fn set_network(&mut self, from: NodeId, to: NodeId, config: ChaosConfig) {
187        let seed = self.seed_counter;
188        self.seed_counter += 1;
189        self.networks
190            .insert((from, to), ChaosNetwork::with_seed(config, seed));
191    }
192
193    /// Run simulation for a duration
194    pub fn run(&mut self, duration: Duration) -> SimulationResult {
195        let mut result = SimulationResult::new();
196        let ticks = (duration.as_micros() / self.tick_interval.as_micros()) as u64;
197
198        for _ in 0..ticks {
199            self.tick(&mut result);
200        }
201
202        result
203    }
204
205    /// Execute one simulation tick
206    fn tick(&mut self, result: &mut SimulationResult) {
207        self.global_time += self.tick_interval;
208
209        // Advance all nodes
210        for node in self.nodes.values_mut() {
211            node.tick(self.tick_interval);
212        }
213
214        // Record state
215        result.record_tick(self);
216
217        // Simulate time sync messages between nodes
218        self.simulate_time_sync();
219    }
220
221    /// Simulate time synchronization between nodes
222    fn simulate_time_sync(&mut self) {
223        let node_ids: Vec<NodeId> = self.nodes.keys().copied().collect();
224
225        for from in &node_ids {
226            for to in &node_ids {
227                if from == to {
228                    continue;
229                }
230
231                // Get sender's state time
232                let sender_time = self.nodes.get(from).map(|n| n.tau_s());
233
234                if let Some(sender_time) = sender_time {
235                    // Update receiver's network model
236                    if let Some(receiver) = self.nodes.get_mut(to) {
237                        receiver.time_engine.update_from_packet(
238                            *from,
239                            sender_time,
240                            0, // seq
241                        );
242                    }
243                }
244            }
245        }
246    }
247
248    /// Get a node
249    pub fn node(&self, id: NodeId) -> Option<&SimulatedNode> {
250        self.nodes.get(&id)
251    }
252
253    /// Get mutable node
254    pub fn node_mut(&mut self, id: NodeId) -> Option<&mut SimulatedNode> {
255        self.nodes.get_mut(&id)
256    }
257
258    /// Get global time
259    pub fn global_time(&self) -> Duration {
260        self.global_time
261    }
262
263    /// Get all node IDs
264    pub fn node_ids(&self) -> Vec<NodeId> {
265        self.nodes.keys().copied().collect()
266    }
267}
268
269/// Simulation result and statistics
270#[derive(Debug, Default)]
271pub struct SimulationResult {
272    /// Total ticks executed
273    pub total_ticks: u64,
274    /// Maximum clock divergence observed (microseconds)
275    pub max_divergence_us: i64,
276    /// Average clock divergence (microseconds)
277    pub avg_divergence_us: f64,
278    /// Divergence samples
279    divergence_samples: Vec<i64>,
280    /// Horizon adaptation events
281    pub horizon_changes: u32,
282    /// Time position classifications
283    pub classifications: HashMap<TimePosition, u64>,
284}
285
286impl SimulationResult {
287    pub fn new() -> Self {
288        SimulationResult::default()
289    }
290
291    fn record_tick(&mut self, sim: &TimeSimulator) {
292        self.total_ticks += 1;
293
294        // Calculate divergence between all node pairs
295        let nodes: Vec<_> = sim.nodes.values().collect();
296        if nodes.len() >= 2 {
297            for i in 0..nodes.len() {
298                for j in (i + 1)..nodes.len() {
299                    let t1 = nodes[i].tau_s().as_micros();
300                    let t2 = nodes[j].tau_s().as_micros();
301                    let divergence = (t1 - t2).abs();
302
303                    self.divergence_samples.push(divergence);
304                    self.max_divergence_us = self.max_divergence_us.max(divergence);
305                }
306            }
307        }
308    }
309
310    /// Calculate final statistics
311    pub fn finalize(&mut self) {
312        if !self.divergence_samples.is_empty() {
313            let sum: i64 = self.divergence_samples.iter().sum();
314            self.avg_divergence_us = sum as f64 / self.divergence_samples.len() as f64;
315        }
316    }
317
318    /// Get divergence in milliseconds
319    pub fn max_divergence_ms(&self) -> f64 {
320        self.max_divergence_us as f64 / 1000.0
321    }
322
323    /// Get average divergence in milliseconds
324    pub fn avg_divergence_ms(&self) -> f64 {
325        self.avg_divergence_us / 1000.0
326    }
327}
328
329/// Predefined test scenarios
330pub mod scenarios {
331    use super::*;
332
333    /// Two nodes with perfect clocks
334    pub fn perfect_pair() -> TimeSimulator {
335        let mut sim = TimeSimulator::new(Duration::from_millis(10));
336        sim.add_node(NodeId::new(1));
337        sim.add_node(NodeId::new(2));
338        sim
339    }
340
341    /// Two nodes with drifting clocks
342    pub fn drifting_pair() -> TimeSimulator {
343        let mut sim = TimeSimulator::new(Duration::from_millis(10));
344        sim.add_node_with_drift(NodeId::new(1), ClockDriftModel::fast());
345        sim.add_node_with_drift(NodeId::new(2), ClockDriftModel::slow());
346        sim
347    }
348
349    /// Small swarm with varying clock quality
350    pub fn small_swarm(count: usize) -> TimeSimulator {
351        let mut sim = TimeSimulator::new(Duration::from_millis(10));
352
353        for i in 0..count {
354            let drift = match i % 4 {
355                0 => ClockDriftModel::perfect(),
356                1 => ClockDriftModel::fast(),
357                2 => ClockDriftModel::slow(),
358                _ => ClockDriftModel::unstable(),
359            };
360            sim.add_node_with_drift(NodeId::new(i as u64), drift);
361        }
362
363        sim
364    }
365
366    /// Hostile network scenario
367    pub fn hostile_network() -> TimeSimulator {
368        let mut sim = TimeSimulator::new(Duration::from_millis(10));
369
370        let node1 = NodeId::new(1);
371        let node2 = NodeId::new(2);
372
373        sim.add_node_with_drift(node1, ClockDriftModel::unstable());
374        sim.add_node_with_drift(node2, ClockDriftModel::unstable());
375
376        sim.set_network(node1, node2, ChaosConfig::hostile());
377        sim.set_network(node2, node1, ChaosConfig::hostile());
378
379        sim
380    }
381}
382
383#[cfg(test)]
384mod tests {
385    use super::*;
386
387    #[test]
388    fn test_perfect_clocks() {
389        let mut sim = scenarios::perfect_pair();
390        let mut result = sim.run(Duration::from_secs(10));
391        result.finalize();
392
393        println!(
394            "Perfect clocks - Max divergence: {:.3}ms",
395            result.max_divergence_ms()
396        );
397        // Perfect clocks should have minimal divergence
398        assert!(result.max_divergence_ms() < 100.0);
399    }
400
401    #[test]
402    fn test_drifting_clocks() {
403        let mut sim = scenarios::drifting_pair();
404        let mut result = sim.run(Duration::from_secs(60));
405        result.finalize();
406
407        println!(
408            "Drifting clocks - Max divergence: {:.3}ms",
409            result.max_divergence_ms()
410        );
411        // Drifting clocks will diverge but time engine should limit it
412    }
413
414    #[test]
415    fn test_small_swarm() {
416        let mut sim = scenarios::small_swarm(5);
417        let mut result = sim.run(Duration::from_secs(30));
418        result.finalize();
419
420        println!(
421            "Small swarm - Max divergence: {:.3}ms, Avg: {:.3}ms",
422            result.max_divergence_ms(),
423            result.avg_divergence_ms()
424        );
425    }
426
427    #[test]
428    fn test_clock_drift_model() {
429        let mut rng = StdRng::seed_from_u64(42);
430        let mut drift = ClockDriftModel::fast();
431
432        // Apply 1000 ticks of 10ms each
433        for _ in 0..1000 {
434            drift.apply(Duration::from_millis(10), &mut rng);
435        }
436
437        // Fast clock should have positive accumulated drift
438        println!("Accumulated drift: {:?}", drift.accumulated_drift());
439        assert!(drift.accumulated_drift() > Duration::ZERO);
440    }
441
442    #[test]
443    fn test_reality_window_classification() {
444        let mut sim = scenarios::perfect_pair();
445        sim.run(Duration::from_secs(1));
446
447        let node = sim.node(NodeId::new(1)).unwrap();
448        let rw = node.reality_window();
449
450        // Current time should be in Current position
451        let current = node.tau_s();
452        assert!(rw.contains(current));
453        assert_eq!(node.classify(current), TimePosition::Current);
454
455        // Far past should be TooLate
456        let past = StateTime::from_millis(current.as_millis() - 10000);
457        assert_eq!(node.classify(past), TimePosition::TooLate);
458
459        // Far future should be TooEarly
460        let future = StateTime::from_millis(current.as_millis() + 10000);
461        assert_eq!(node.classify(future), TimePosition::TooEarly);
462    }
463}