1pub mod heijunka;
49pub mod jidoka;
50pub mod metamorphic;
51pub mod physics;
52pub mod render;
53pub mod scenarios;
54pub mod units;
55
56#[cfg(feature = "wasm")]
57pub mod wasm;
58
59pub mod prelude {
61 pub use super::heijunka::{
62 FrameResult, HeijunkaConfig, HeijunkaScheduler, HeijunkaStatus, QualityLevel,
63 };
64 pub use super::jidoka::{
65 JidokaResponse, JidokaStatus, OrbitJidokaConfig, OrbitJidokaGuard, OrbitJidokaViolation,
66 };
67 pub use super::metamorphic::{
68 run_all_metamorphic_tests, test_angular_momentum_conservation, test_deterministic_replay,
69 test_energy_conservation, test_rotation_invariance, test_time_reversal, MetamorphicResult,
70 };
71 pub use super::physics::{AdaptiveIntegrator, NBodyState, OrbitBody, YoshidaIntegrator};
72 pub use super::scenarios::{
73 BodyConfig, HohmannConfig, KeplerConfig, LagrangeConfig, LagrangePoint, NBodyConfig,
74 ScenarioType,
75 };
76 pub use super::units::{
77 Acceleration3D, OrbitMass, OrbitTime, Position3D, Velocity3D, AU, EARTH_MASS, G, SOLAR_MASS,
78 };
79}
80
81#[must_use]
107pub fn run_simulation(
108 scenario: &scenarios::ScenarioType,
109 duration_seconds: f64,
110 dt_seconds: f64,
111 softening: f64,
112) -> SimulationResult {
113 use jidoka::{JidokaResponse, OrbitJidokaConfig, OrbitJidokaGuard};
114 use physics::YoshidaIntegrator;
115 use units::OrbitTime;
116
117 let mut state = match scenario {
119 scenarios::ScenarioType::Kepler(config) => config.build(softening),
120 scenarios::ScenarioType::NBody(config) => config.build(softening),
121 scenarios::ScenarioType::Hohmann(config) => config.build_initial(softening),
122 scenarios::ScenarioType::Lagrange(config) => config.build(softening),
123 };
124
125 let mut jidoka = OrbitJidokaGuard::new(OrbitJidokaConfig::default());
127 jidoka.initialize(&state);
128
129 let yoshida = YoshidaIntegrator::new();
131 let dt = OrbitTime::from_seconds(dt_seconds);
132
133 let initial_energy = state.total_energy();
135 let initial_angular_momentum = state.angular_momentum_magnitude();
136 let mut steps = 0u64;
137 let mut warnings = 0u64;
138 let mut paused = false;
139
140 let num_steps = (duration_seconds / dt_seconds) as u64;
142 for _ in 0..num_steps {
143 if yoshida.step(&mut state, dt).is_err() {
145 paused = true;
146 break;
147 }
148
149 steps += 1;
150
151 let response = jidoka.check(&state);
153 match response {
154 JidokaResponse::Continue => {}
155 JidokaResponse::Warning { .. } => {
156 warnings += 1;
157 }
158 JidokaResponse::Pause { .. } | JidokaResponse::Halt { .. } => {
159 paused = true;
160 break;
161 }
162 }
163 }
164
165 let final_energy = state.total_energy();
167 let final_angular_momentum = state.angular_momentum_magnitude();
168 let energy_error = (final_energy - initial_energy).abs() / initial_energy.abs();
169 let angular_momentum_error =
170 (final_angular_momentum - initial_angular_momentum).abs() / initial_angular_momentum.abs();
171
172 SimulationResult {
173 final_state: state,
174 steps,
175 warnings,
176 paused,
177 energy_error,
178 angular_momentum_error,
179 sim_time: steps as f64 * dt_seconds,
180 }
181}
182
183#[derive(Debug, Clone)]
185pub struct SimulationResult {
186 pub final_state: physics::NBodyState,
188 pub steps: u64,
190 pub warnings: u64,
192 pub paused: bool,
194 pub energy_error: f64,
196 pub angular_momentum_error: f64,
198 pub sim_time: f64,
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205 use scenarios::{KeplerConfig, ScenarioType};
206
207 #[test]
208 fn test_run_simulation_kepler() {
209 let result = run_simulation(
210 &ScenarioType::Kepler(KeplerConfig::earth_sun()),
211 86400.0 * 10.0, 3600.0, 1e6,
214 );
215
216 assert!(result.steps > 0);
217 assert!(!result.paused);
218 assert!(
219 result.energy_error < 1e-6,
220 "Energy error: {}",
221 result.energy_error
222 );
223 }
224
225 #[test]
226 fn test_run_simulation_nbody() {
227 let result = run_simulation(
228 &ScenarioType::NBody(scenarios::NBodyConfig::inner_solar_system()),
229 86400.0, 3600.0, 1e9, );
233
234 assert!(result.steps > 0);
235 assert!(result.energy_error < 1e-4);
236 }
237
238 #[test]
239 fn test_prelude_imports() {
240 use prelude::*;
241
242 let pos = Position3D::from_au(1.0, 0.0, 0.0);
243 let _vel = Velocity3D::from_mps(0.0, 29780.0, 0.0);
244 let _mass = OrbitMass::from_solar_masses(1.0);
245 let _time = OrbitTime::from_days(365.25);
246 let _config = KeplerConfig::earth_sun();
247
248 assert!(pos.is_finite());
249 }
250
251 #[test]
252 fn test_run_simulation_hohmann() {
253 let result = run_simulation(
254 &ScenarioType::Hohmann(scenarios::HohmannConfig::earth_to_mars()),
255 86400.0 * 10.0, 3600.0, 1e6,
258 );
259
260 assert!(result.steps > 0);
261 assert!(!result.paused);
262 }
263
264 #[test]
265 fn test_run_simulation_lagrange() {
266 let result = run_simulation(
267 &ScenarioType::Lagrange(scenarios::LagrangeConfig::sun_earth_l2()),
268 86400.0, 3600.0, 1e9,
271 );
272
273 assert!(result.steps > 0);
274 }
275
276 #[test]
277 fn test_simulation_result_fields() {
278 let result = run_simulation(
279 &ScenarioType::Kepler(KeplerConfig::earth_sun()),
280 3600.0, 3600.0, 1e6,
283 );
284
285 assert_eq!(result.steps, 1);
286 assert_eq!(result.warnings, 0);
287 assert!(!result.paused);
288 assert!(result.sim_time > 0.0);
289 assert!(result.final_state.num_bodies() == 2);
290 }
291}