1use 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#[derive(Clone, Debug)]
21pub struct ClockDriftModel {
22 pub drift_rate: f64,
24 pub jitter_us: u32,
26 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 pub fn perfect() -> Self {
41 Self::new(1.0, 0)
42 }
43
44 pub fn fast() -> Self {
46 Self::new(1.0001, 50)
47 }
48
49 pub fn slow() -> Self {
51 Self::new(0.9999, 50)
52 }
53
54 pub fn unstable() -> Self {
56 Self::new(1.0, 500)
57 }
58
59 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 pub fn accumulated_drift(&self) -> Duration {
75 Duration::from_micros(self.accumulated_drift.unsigned_abs())
76 }
77}
78
79pub struct SimulatedNode {
81 pub node_id: NodeId,
83 pub time_engine: TimeEngine,
85 pub drift_model: ClockDriftModel,
87 rng: StdRng,
89 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 pub fn tick(&mut self, real_dt: Duration) {
111 let _local_dt = self.drift_model.apply(real_dt, &mut self.rng);
113
114 self.time_engine.tick();
116 self.tick_count += 1;
117 }
118
119 pub fn tau_p(&self) -> PerceptualTime {
121 self.time_engine.tau_p()
122 }
123
124 pub fn tau_s(&self) -> StateTime {
126 self.time_engine.tau_s()
127 }
128
129 pub fn reality_window(&self) -> RealityWindow {
131 self.time_engine.reality_window()
132 }
133
134 pub fn classify(&self, t: StateTime) -> TimePosition {
136 self.time_engine.classify_time(t)
137 }
138
139 pub fn tick_count(&self) -> u64 {
141 self.tick_count
142 }
143}
144
145pub struct TimeSimulator {
147 nodes: HashMap<NodeId, SimulatedNode>,
149 networks: HashMap<(NodeId, NodeId), ChaosNetwork>,
151 global_time: Duration,
153 tick_interval: Duration,
155 seed_counter: u64,
157}
158
159impl TimeSimulator {
160 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 pub fn add_node(&mut self, node_id: NodeId) {
173 self.add_node_with_drift(node_id, ClockDriftModel::perfect());
174 }
175
176 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 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 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 fn tick(&mut self, result: &mut SimulationResult) {
207 self.global_time += self.tick_interval;
208
209 for node in self.nodes.values_mut() {
211 node.tick(self.tick_interval);
212 }
213
214 result.record_tick(self);
216
217 self.simulate_time_sync();
219 }
220
221 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 let sender_time = self.nodes.get(from).map(|n| n.tau_s());
233
234 if let Some(sender_time) = sender_time {
235 if let Some(receiver) = self.nodes.get_mut(to) {
237 receiver.time_engine.update_from_packet(
238 *from,
239 sender_time,
240 0, );
242 }
243 }
244 }
245 }
246 }
247
248 pub fn node(&self, id: NodeId) -> Option<&SimulatedNode> {
250 self.nodes.get(&id)
251 }
252
253 pub fn node_mut(&mut self, id: NodeId) -> Option<&mut SimulatedNode> {
255 self.nodes.get_mut(&id)
256 }
257
258 pub fn global_time(&self) -> Duration {
260 self.global_time
261 }
262
263 pub fn node_ids(&self) -> Vec<NodeId> {
265 self.nodes.keys().copied().collect()
266 }
267}
268
269#[derive(Debug, Default)]
271pub struct SimulationResult {
272 pub total_ticks: u64,
274 pub max_divergence_us: i64,
276 pub avg_divergence_us: f64,
278 divergence_samples: Vec<i64>,
280 pub horizon_changes: u32,
282 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 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 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 pub fn max_divergence_ms(&self) -> f64 {
320 self.max_divergence_us as f64 / 1000.0
321 }
322
323 pub fn avg_divergence_ms(&self) -> f64 {
325 self.avg_divergence_us / 1000.0
326 }
327}
328
329pub mod scenarios {
331 use super::*;
332
333 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 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 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 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 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 }
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 for _ in 0..1000 {
434 drift.apply(Duration::from_millis(10), &mut rng);
435 }
436
437 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 let current = node.tau_s();
452 assert!(rw.contains(current));
453 assert_eq!(node.classify(current), TimePosition::Current);
454
455 let past = StateTime::from_millis(current.as_millis() - 10000);
457 assert_eq!(node.classify(past), TimePosition::TooLate);
458
459 let future = StateTime::from_millis(current.as_millis() + 10000);
461 assert_eq!(node.classify(future), TimePosition::TooEarly);
462 }
463}