Skip to main content

simular/engine/
mod.rs

1//! Core simulation engine.
2//!
3//! Implements the central simulation loop with:
4//! - Deterministic RNG (PCG with partitioned seeds)
5//! - Event scheduling with deterministic ordering
6//! - Jidoka guards for stop-on-error
7//! - State management
8
9pub mod clock;
10pub mod jidoka;
11pub mod rng;
12pub mod scheduler;
13pub mod state;
14
15use serde::{Deserialize, Serialize};
16
17pub use clock::SimClock;
18pub use jidoka::{JidokaGuard, JidokaViolation};
19pub use rng::SimRng;
20pub use scheduler::{EventScheduler, ScheduledEvent};
21pub use state::SimState;
22
23use crate::config::{IntegratorType, PhysicsEngine as ConfigPhysicsEngine, SimConfig};
24use crate::domains::physics::{
25    CentralForceField, EulerIntegrator, ForceField, GravityField, Integrator, PhysicsEngine,
26    RK4Integrator, VerletIntegrator,
27};
28use crate::engine::state::Vec3;
29use crate::error::SimResult;
30
31/// Simulation time representation.
32///
33/// Uses a fixed-point representation for reproducibility across platforms.
34/// Internal representation is in nanoseconds to avoid floating-point issues.
35#[derive(
36    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default,
37)]
38pub struct SimTime {
39    /// Time in nanoseconds from simulation start.
40    nanos: u64,
41}
42
43impl SimTime {
44    /// Zero time (simulation start).
45    pub const ZERO: Self = Self { nanos: 0 };
46
47    /// Create time from seconds.
48    ///
49    /// # Panics
50    ///
51    /// Panics if seconds is negative or not finite.
52    #[must_use]
53    pub fn from_secs(secs: f64) -> Self {
54        assert!(secs >= 0.0, "SimTime cannot be negative");
55        assert!(secs.is_finite(), "SimTime must be finite");
56        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
57        let nanos = (secs * 1_000_000_000.0) as u64;
58        Self { nanos }
59    }
60
61    /// Create time from nanoseconds.
62    #[must_use]
63    pub const fn from_nanos(nanos: u64) -> Self {
64        Self { nanos }
65    }
66
67    /// Get time as seconds (f64).
68    #[must_use]
69    pub fn as_secs_f64(&self) -> f64 {
70        self.nanos as f64 / 1_000_000_000.0
71    }
72
73    /// Get time as nanoseconds.
74    #[must_use]
75    pub const fn as_nanos(&self) -> u64 {
76        self.nanos
77    }
78
79    /// Add duration to time.
80    #[must_use]
81    pub const fn add_nanos(self, nanos: u64) -> Self {
82        Self {
83            nanos: self.nanos + nanos,
84        }
85    }
86
87    /// Subtract duration from time, saturating at zero.
88    #[must_use]
89    pub const fn saturating_sub_nanos(self, nanos: u64) -> Self {
90        Self {
91            nanos: self.nanos.saturating_sub(nanos),
92        }
93    }
94}
95
96impl std::ops::Add for SimTime {
97    type Output = Self;
98
99    fn add(self, rhs: Self) -> Self::Output {
100        Self {
101            nanos: self.nanos + rhs.nanos,
102        }
103    }
104}
105
106impl std::ops::Sub for SimTime {
107    type Output = Self;
108
109    fn sub(self, rhs: Self) -> Self::Output {
110        Self {
111            nanos: self.nanos.saturating_sub(rhs.nanos),
112        }
113    }
114}
115
116impl std::fmt::Display for SimTime {
117    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118        write!(f, "{:.9}s", self.as_secs_f64())
119    }
120}
121
122/// Main simulation engine.
123///
124/// Coordinates all subsystems:
125/// - Event scheduling
126/// - State management
127/// - Jidoka monitoring
128/// - Checkpointing
129/// - Physics simulation
130pub struct SimEngine {
131    /// Current simulation state.
132    state: SimState,
133    /// Event scheduler.
134    scheduler: EventScheduler,
135    /// Jidoka guard for anomaly detection.
136    jidoka: JidokaGuard,
137    /// Simulation clock.
138    clock: SimClock,
139    /// Random number generator.
140    rng: SimRng,
141    /// Physics engine (optional).
142    physics: Option<PhysicsEngine>,
143    /// Configuration (stored for reference and future use).
144    #[allow(dead_code)]
145    config: SimConfig,
146}
147
148impl SimEngine {
149    /// Create a new simulation engine from configuration.
150    ///
151    /// # Errors
152    ///
153    /// Returns error if configuration validation fails.
154    pub fn new(config: SimConfig) -> SimResult<Self> {
155        let seed = config.reproducibility.seed;
156        let rng = SimRng::new(seed);
157        let jidoka = JidokaGuard::from_config(&config);
158        let clock = SimClock::new(config.get_timestep());
159
160        // Initialize Physics Engine based on config
161        let physics = if config.domains.physics.enabled {
162            let integrator: Box<dyn Integrator + Send + Sync> =
163                match config.domains.physics.integrator.integrator_type {
164                    IntegratorType::Euler => Box::new(EulerIntegrator::new()),
165                    IntegratorType::Rk4 => Box::new(RK4Integrator::new()),
166                    // Default to Verlet for Verlet, Rk78, SymplecticEuler, etc.
167                    _ => Box::new(VerletIntegrator::new()),
168                };
169
170            let force_field: Box<dyn ForceField + Send + Sync> = match config.domains.physics.engine
171            {
172                ConfigPhysicsEngine::Orbital => Box::new(CentralForceField::new(1.0, Vec3::zero())), // Default mu=1.0 for now
173                // Default to GravityField for RigidBody, Fluid, Discrete, etc.
174                _ => Box::new(GravityField::default()),
175            };
176
177            Some(PhysicsEngine::new_boxed(force_field, integrator))
178        } else {
179            None
180        };
181
182        Ok(Self {
183            state: SimState::default(),
184            scheduler: EventScheduler::new(),
185            jidoka,
186            clock,
187            rng,
188            physics,
189            config,
190        })
191    }
192
193    /// Get current simulation time.
194    #[must_use]
195    #[allow(clippy::missing_const_for_fn)] // Delegating to non-const method
196    pub fn current_time(&self) -> SimTime {
197        self.clock.current_time()
198    }
199
200    /// Get current simulation state.
201    #[must_use]
202    pub const fn state(&self) -> &SimState {
203        &self.state
204    }
205
206    /// Get mutable reference to state.
207    #[must_use]
208    pub fn state_mut(&mut self) -> &mut SimState {
209        &mut self.state
210    }
211
212    /// Get reference to RNG.
213    #[must_use]
214    pub const fn rng(&self) -> &SimRng {
215        &self.rng
216    }
217
218    /// Get mutable reference to RNG.
219    #[must_use]
220    pub fn rng_mut(&mut self) -> &mut SimRng {
221        &mut self.rng
222    }
223
224    /// Step the simulation forward by one timestep.
225    ///
226    /// # Errors
227    ///
228    /// Returns `SimError` if:
229    /// - Jidoka violation detected (NaN, energy drift, constraint)
230    /// - Domain engine error
231    pub fn step(&mut self) -> SimResult<()> {
232        // Advance clock
233        self.clock.tick();
234
235        // Process scheduled events
236        while let Some(event) = self.scheduler.next_before(self.clock.current_time()) {
237            self.state.apply_event(&event.event)?;
238        }
239
240        // Run Physics Step
241        if let Some(physics) = &self.physics {
242            physics.step(&mut self.state, self.clock.dt())?;
243        }
244
245        // Jidoka check (stop-on-error)
246        self.jidoka.check(&self.state)?;
247
248        Ok(())
249    }
250
251    /// Run simulation for specified duration.
252    ///
253    /// # Errors
254    ///
255    /// Returns error if any step fails.
256    pub fn run_for(&mut self, duration: SimTime) -> SimResult<()> {
257        let end_time = self.clock.current_time() + duration;
258
259        while self.clock.current_time() < end_time {
260            self.step()?;
261        }
262
263        Ok(())
264    }
265
266    /// Run simulation until predicate returns true.
267    ///
268    /// # Errors
269    ///
270    /// Returns error if any step fails.
271    pub fn run_until<F>(&mut self, predicate: F) -> SimResult<()>
272    where
273        F: Fn(&SimState) -> bool,
274    {
275        while !predicate(&self.state) {
276            self.step()?;
277        }
278
279        Ok(())
280    }
281}
282
283#[cfg(test)]
284#[allow(clippy::unwrap_used, clippy::expect_used)]
285mod tests {
286    use super::*;
287    use crate::config::SimConfig;
288
289    #[test]
290    fn test_sim_time_creation() {
291        let t1 = SimTime::from_secs(1.5);
292        assert!((t1.as_secs_f64() - 1.5).abs() < 1e-9);
293
294        let t2 = SimTime::from_nanos(1_500_000_000);
295        assert_eq!(t1, t2);
296    }
297
298    #[test]
299    fn test_sim_time_arithmetic() {
300        let t1 = SimTime::from_secs(1.0);
301        let t2 = SimTime::from_secs(0.5);
302
303        let sum = t1 + t2;
304        assert!((sum.as_secs_f64() - 1.5).abs() < 1e-9);
305
306        let diff = t1 - t2;
307        assert!((diff.as_secs_f64() - 0.5).abs() < 1e-9);
308    }
309
310    #[test]
311    fn test_sim_time_ordering() {
312        let t1 = SimTime::from_secs(1.0);
313        let t2 = SimTime::from_secs(2.0);
314
315        assert!(t1 < t2);
316        assert!(t2 > t1);
317        assert_eq!(t1, t1);
318    }
319
320    #[test]
321    fn test_sim_time_display() {
322        let t = SimTime::from_secs(1.234_567_890);
323        let s = t.to_string();
324        assert!(s.contains("1.234567890"));
325    }
326
327    #[test]
328    fn test_sim_time_zero() {
329        let t = SimTime::ZERO;
330        assert_eq!(t.as_nanos(), 0);
331        assert!((t.as_secs_f64() - 0.0).abs() < f64::EPSILON);
332    }
333
334    #[test]
335    fn test_sim_time_add_nanos() {
336        let t = SimTime::from_secs(1.0);
337        let t2 = t.add_nanos(500_000_000);
338        assert!((t2.as_secs_f64() - 1.5).abs() < 1e-9);
339    }
340
341    #[test]
342    fn test_sim_time_saturating_sub() {
343        let t = SimTime::from_secs(1.0);
344        let t2 = t.saturating_sub_nanos(500_000_000);
345        assert!((t2.as_secs_f64() - 0.5).abs() < 1e-9);
346
347        // Saturating at zero
348        let t3 = t.saturating_sub_nanos(2_000_000_000);
349        assert_eq!(t3.as_nanos(), 0);
350    }
351
352    #[test]
353    fn test_sim_time_default() {
354        let t: SimTime = Default::default();
355        assert_eq!(t.as_nanos(), 0);
356    }
357
358    #[test]
359    fn test_sim_time_hash() {
360        use std::collections::HashSet;
361        let mut set = HashSet::new();
362        set.insert(SimTime::from_secs(1.0));
363        set.insert(SimTime::from_secs(2.0));
364        set.insert(SimTime::from_secs(1.0)); // Duplicate
365        assert_eq!(set.len(), 2);
366    }
367
368    #[test]
369    fn test_sim_time_clone() {
370        let t1 = SimTime::from_secs(1.0);
371        let t2 = t1;
372        assert_eq!(t1, t2);
373    }
374
375    #[test]
376    fn test_sim_time_sub_saturating() {
377        let t1 = SimTime::from_secs(1.0);
378        let t2 = SimTime::from_secs(2.0);
379        // Sub uses saturating_sub
380        let diff = t1 - t2;
381        assert_eq!(diff.as_nanos(), 0);
382    }
383
384    #[test]
385    fn test_sim_engine_new() {
386        let config = SimConfig::builder().seed(42).build();
387        let engine = SimEngine::new(config);
388        assert!(engine.is_ok());
389    }
390
391    #[test]
392    fn test_sim_engine_initial_time() {
393        let config = SimConfig::builder().seed(42).build();
394        let engine = SimEngine::new(config).unwrap();
395        assert_eq!(engine.current_time(), SimTime::ZERO);
396    }
397
398    #[test]
399    fn test_sim_engine_state() {
400        let config = SimConfig::builder().seed(42).build();
401        let engine = SimEngine::new(config).unwrap();
402        let state = engine.state();
403        assert_eq!(state.num_bodies(), 0);
404    }
405
406    #[test]
407    fn test_sim_engine_state_mut() {
408        let config = SimConfig::builder().seed(42).build();
409        let mut engine = SimEngine::new(config).unwrap();
410        let state = engine.state_mut();
411        // Add a body to test mutability
412        state.add_body(1.0, state::Vec3::zero(), state::Vec3::zero());
413        assert_eq!(state.num_bodies(), 1);
414    }
415
416    #[test]
417    fn test_sim_engine_rng() {
418        let config = SimConfig::builder().seed(42).build();
419        let engine = SimEngine::new(config).unwrap();
420        let _rng = engine.rng();
421    }
422
423    #[test]
424    fn test_sim_engine_rng_mut() {
425        let config = SimConfig::builder().seed(42).build();
426        let mut engine = SimEngine::new(config).unwrap();
427        let rng = engine.rng_mut();
428        let _ = rng.gen_f64();
429    }
430
431    #[test]
432    fn test_sim_engine_step() {
433        let config = SimConfig::builder().seed(42).build();
434        let mut engine = SimEngine::new(config).unwrap();
435        let result = engine.step();
436        assert!(result.is_ok());
437        assert!(engine.current_time() > SimTime::ZERO);
438    }
439
440    #[test]
441    fn test_sim_engine_multiple_steps() {
442        let config = SimConfig::builder().seed(42).build();
443        let mut engine = SimEngine::new(config).unwrap();
444
445        for _ in 0..10 {
446            engine.step().unwrap();
447        }
448
449        assert!(engine.current_time() > SimTime::ZERO);
450    }
451
452    #[test]
453    fn test_sim_engine_run_for() {
454        let config = SimConfig::builder().seed(42).build();
455        let mut engine = SimEngine::new(config).unwrap();
456
457        let duration = SimTime::from_secs(0.1);
458        let result = engine.run_for(duration);
459        assert!(result.is_ok());
460        assert!(engine.current_time() >= duration);
461    }
462
463    #[test]
464    fn test_sim_engine_run_until() {
465        let config = SimConfig::builder().seed(42).timestep(0.001).build();
466        let mut engine = SimEngine::new(config).unwrap();
467
468        // Add a body so we have something to check
469        engine
470            .state_mut()
471            .add_body(1.0, state::Vec3::zero(), state::Vec3::zero());
472
473        // run_until checks predicate based on state
474        // Stop when bodies exist (immediate)
475        let result = engine.run_until(|state| state.num_bodies() > 0);
476
477        // This is a test that it runs without error
478        assert!(result.is_ok());
479    }
480
481    #[test]
482    fn test_sim_engine_run_until_immediate() {
483        let config = SimConfig::builder().seed(42).build();
484        let mut engine = SimEngine::new(config).unwrap();
485
486        // Predicate immediately true
487        let result = engine.run_until(|_state| true);
488        assert!(result.is_ok());
489        assert_eq!(engine.current_time(), SimTime::ZERO);
490    }
491}