1pub 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#[derive(
36 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default,
37)]
38pub struct SimTime {
39 nanos: u64,
41}
42
43impl SimTime {
44 pub const ZERO: Self = Self { nanos: 0 };
46
47 #[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 #[must_use]
63 pub const fn from_nanos(nanos: u64) -> Self {
64 Self { nanos }
65 }
66
67 #[must_use]
69 pub fn as_secs_f64(&self) -> f64 {
70 self.nanos as f64 / 1_000_000_000.0
71 }
72
73 #[must_use]
75 pub const fn as_nanos(&self) -> u64 {
76 self.nanos
77 }
78
79 #[must_use]
81 pub const fn add_nanos(self, nanos: u64) -> Self {
82 Self {
83 nanos: self.nanos + nanos,
84 }
85 }
86
87 #[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
122pub struct SimEngine {
131 state: SimState,
133 scheduler: EventScheduler,
135 jidoka: JidokaGuard,
137 clock: SimClock,
139 rng: SimRng,
141 physics: Option<PhysicsEngine>,
143 #[allow(dead_code)]
145 config: SimConfig,
146}
147
148impl SimEngine {
149 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 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 _ => 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())), _ => 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 #[must_use]
195 #[allow(clippy::missing_const_for_fn)] pub fn current_time(&self) -> SimTime {
197 self.clock.current_time()
198 }
199
200 #[must_use]
202 pub const fn state(&self) -> &SimState {
203 &self.state
204 }
205
206 #[must_use]
208 pub fn state_mut(&mut self) -> &mut SimState {
209 &mut self.state
210 }
211
212 #[must_use]
214 pub const fn rng(&self) -> &SimRng {
215 &self.rng
216 }
217
218 #[must_use]
220 pub fn rng_mut(&mut self) -> &mut SimRng {
221 &mut self.rng
222 }
223
224 pub fn step(&mut self) -> SimResult<()> {
232 self.clock.tick();
234
235 while let Some(event) = self.scheduler.next_before(self.clock.current_time()) {
237 self.state.apply_event(&event.event)?;
238 }
239
240 if let Some(physics) = &self.physics {
242 physics.step(&mut self.state, self.clock.dt())?;
243 }
244
245 self.jidoka.check(&self.state)?;
247
248 Ok(())
249 }
250
251 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 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 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)); 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 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 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 engine
470 .state_mut()
471 .add_body(1.0, state::Vec3::zero(), state::Vec3::zero());
472
473 let result = engine.run_until(|state| state.num_bodies() > 0);
476
477 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 let result = engine.run_until(|_state| true);
488 assert!(result.is_ok());
489 assert_eq!(engine.current_time(), SimTime::ZERO);
490 }
491}