1use crate::orbit::physics::{NBodyState, OrbitBody, YoshidaIntegrator};
20use crate::orbit::units::{OrbitMass, OrbitTime, Position3D, Velocity3D};
21
22#[derive(Debug, Clone)]
24pub struct MetamorphicResult {
25 pub relation: String,
27 pub passed: bool,
29 pub error: f64,
31 pub tolerance: f64,
33 pub details: String,
35}
36
37impl MetamorphicResult {
38 #[must_use]
40 pub fn pass(relation: &str, error: f64, tolerance: f64) -> Self {
41 Self {
42 relation: relation.to_string(),
43 passed: true,
44 error,
45 tolerance,
46 details: String::new(),
47 }
48 }
49
50 #[must_use]
52 pub fn fail(relation: &str, error: f64, tolerance: f64, details: &str) -> Self {
53 Self {
54 relation: relation.to_string(),
55 passed: false,
56 error,
57 tolerance,
58 details: details.to_string(),
59 }
60 }
61}
62
63fn compute_pairwise_distances(state: &NBodyState) -> Vec<f64> {
65 let n = state.bodies.len();
66 let mut distances = Vec::with_capacity(n * (n - 1) / 2);
67
68 for i in 0..n {
69 for j in (i + 1)..n {
70 let (x1, y1, z1) = state.bodies[i].position.as_meters();
71 let (x2, y2, z2) = state.bodies[j].position.as_meters();
72 let dx = x2 - x1;
73 let dy = y2 - y1;
74 let dz = z2 - z1;
75 distances.push((dx * dx + dy * dy + dz * dz).sqrt());
76 }
77 }
78
79 distances
80}
81
82fn rotate_z(x: f64, y: f64, z: f64, angle: f64) -> (f64, f64, f64) {
84 let (sin_a, cos_a) = angle.sin_cos();
85 (x * cos_a - y * sin_a, x * sin_a + y * cos_a, z)
86}
87
88fn rotate_state(state: &NBodyState, angle: f64) -> NBodyState {
90 let rotated_bodies: Vec<OrbitBody> = state
91 .bodies
92 .iter()
93 .map(|body| {
94 let (px, py, pz) = body.position.as_meters();
95 let (vx, vy, vz) = body.velocity.as_mps();
96
97 let (rpx, rpy, rpz) = rotate_z(px, py, pz, angle);
98 let (rvx, rvy, rvz) = rotate_z(vx, vy, vz, angle);
99
100 OrbitBody::new(
101 body.mass,
102 Position3D::from_meters(rpx, rpy, rpz),
103 Velocity3D::from_mps(rvx, rvy, rvz),
104 )
105 })
106 .collect();
107
108 NBodyState::new(rotated_bodies, state.min_separation().min(1e6))
109}
110
111fn reverse_velocities(state: &NBodyState) -> NBodyState {
113 let reversed_bodies: Vec<OrbitBody> = state
114 .bodies
115 .iter()
116 .map(|body| {
117 let (vx, vy, vz) = body.velocity.as_mps();
118 OrbitBody::new(
119 body.mass,
120 body.position,
121 Velocity3D::from_mps(-vx, -vy, -vz),
122 )
123 })
124 .collect();
125
126 NBodyState::new(reversed_bodies, state.min_separation().min(1e6))
127}
128
129#[allow(dead_code)] fn scale_masses(state: &NBodyState, factor: f64) -> NBodyState {
132 let scaled_bodies: Vec<OrbitBody> = state
133 .bodies
134 .iter()
135 .map(|body| {
136 OrbitBody::new(
137 OrbitMass::from_kg(body.mass.as_kg() * factor),
138 body.position,
139 body.velocity,
140 )
141 })
142 .collect();
143
144 NBodyState::new(scaled_bodies, state.min_separation().min(1e6))
145}
146
147#[must_use]
152pub fn test_rotation_invariance(
153 initial_state: &NBodyState,
154 steps: usize,
155 dt: f64,
156 tolerance: f64,
157) -> MetamorphicResult {
158 let angle = std::f64::consts::PI / 4.0; let mut original = initial_state.clone();
162 let yoshida = YoshidaIntegrator::new();
163 let dt_orbit = OrbitTime::from_seconds(dt);
164
165 for _ in 0..steps {
166 if yoshida.step(&mut original, dt_orbit).is_err() {
167 return MetamorphicResult::fail(
168 "Rotation Invariance",
169 f64::NAN,
170 tolerance,
171 "Original simulation failed",
172 );
173 }
174 }
175
176 let rotated_initial = rotate_state(initial_state, angle);
178 let mut rotated = rotated_initial;
179
180 for _ in 0..steps {
181 if yoshida.step(&mut rotated, dt_orbit).is_err() {
182 return MetamorphicResult::fail(
183 "Rotation Invariance",
184 f64::NAN,
185 tolerance,
186 "Rotated simulation failed",
187 );
188 }
189 }
190
191 let original_distances = compute_pairwise_distances(&original);
193 let rotated_distances = compute_pairwise_distances(&rotated);
194
195 let mut max_error = 0.0_f64;
196 for (d1, d2) in original_distances.iter().zip(rotated_distances.iter()) {
197 let rel_error = if *d1 > f64::EPSILON {
198 (d1 - d2).abs() / d1
199 } else {
200 (d1 - d2).abs()
201 };
202 max_error = max_error.max(rel_error);
203 }
204
205 if max_error <= tolerance {
206 MetamorphicResult::pass("Rotation Invariance", max_error, tolerance)
207 } else {
208 MetamorphicResult::fail(
209 "Rotation Invariance",
210 max_error,
211 tolerance,
212 &format!("Max distance error: {max_error:.2e}"),
213 )
214 }
215}
216
217#[must_use]
222pub fn test_time_reversal(
223 initial_state: &NBodyState,
224 steps: usize,
225 dt: f64,
226 tolerance: f64,
227) -> MetamorphicResult {
228 let yoshida = YoshidaIntegrator::new();
229 let dt_orbit = OrbitTime::from_seconds(dt);
230
231 let mut state = initial_state.clone();
233 for _ in 0..steps {
234 if yoshida.step(&mut state, dt_orbit).is_err() {
235 return MetamorphicResult::fail(
236 "Time-Reversal Symmetry",
237 f64::NAN,
238 tolerance,
239 "Forward simulation failed",
240 );
241 }
242 }
243
244 state = reverse_velocities(&state);
246
247 for _ in 0..steps {
249 if yoshida.step(&mut state, dt_orbit).is_err() {
250 return MetamorphicResult::fail(
251 "Time-Reversal Symmetry",
252 f64::NAN,
253 tolerance,
254 "Backward simulation failed",
255 );
256 }
257 }
258
259 state = reverse_velocities(&state);
261
262 let mut max_pos_error = 0.0_f64;
264 let mut max_vel_error = 0.0_f64;
265
266 for (initial, final_body) in initial_state.bodies.iter().zip(state.bodies.iter()) {
267 let (ix, iy, iz) = initial.position.as_meters();
268 let (fx, fy, fz) = final_body.position.as_meters();
269
270 let initial_r = (ix * ix + iy * iy + iz * iz).sqrt();
271 let pos_error = ((fx - ix).powi(2) + (fy - iy).powi(2) + (fz - iz).powi(2)).sqrt();
272 let rel_pos_error = if initial_r > f64::EPSILON {
273 pos_error / initial_r
274 } else {
275 pos_error
276 };
277 max_pos_error = max_pos_error.max(rel_pos_error);
278
279 let (ivx, ivy, ivz) = initial.velocity.as_mps();
280 let (fvx, fvy, fvz) = final_body.velocity.as_mps();
281
282 let initial_v = (ivx * ivx + ivy * ivy + ivz * ivz).sqrt();
283 let vel_error = ((fvx - ivx).powi(2) + (fvy - ivy).powi(2) + (fvz - ivz).powi(2)).sqrt();
284 let rel_vel_error = if initial_v > f64::EPSILON {
285 vel_error / initial_v
286 } else {
287 vel_error
288 };
289 max_vel_error = max_vel_error.max(rel_vel_error);
290 }
291
292 let max_error = max_pos_error.max(max_vel_error);
293
294 if max_error <= tolerance {
295 MetamorphicResult::pass("Time-Reversal Symmetry", max_error, tolerance)
296 } else {
297 MetamorphicResult::fail(
298 "Time-Reversal Symmetry",
299 max_error,
300 tolerance,
301 &format!("Pos error: {max_pos_error:.2e}, Vel error: {max_vel_error:.2e}"),
302 )
303 }
304}
305
306#[must_use]
310pub fn test_energy_conservation(
311 initial_state: &NBodyState,
312 steps: usize,
313 dt: f64,
314 tolerance: f64,
315) -> MetamorphicResult {
316 let yoshida = YoshidaIntegrator::new();
317 let dt_orbit = OrbitTime::from_seconds(dt);
318
319 let initial_energy = initial_state.total_energy();
320 let mut state = initial_state.clone();
321 let mut max_error = 0.0_f64;
322
323 for _ in 0..steps {
324 if yoshida.step(&mut state, dt_orbit).is_err() {
325 return MetamorphicResult::fail(
326 "Energy Conservation",
327 f64::NAN,
328 tolerance,
329 "Simulation failed",
330 );
331 }
332
333 let current_energy = state.total_energy();
334 let rel_error = if initial_energy.abs() > f64::EPSILON {
335 (current_energy - initial_energy).abs() / initial_energy.abs()
336 } else {
337 (current_energy - initial_energy).abs()
338 };
339 max_error = max_error.max(rel_error);
340 }
341
342 if max_error <= tolerance {
343 MetamorphicResult::pass("Energy Conservation", max_error, tolerance)
344 } else {
345 MetamorphicResult::fail(
346 "Energy Conservation",
347 max_error,
348 tolerance,
349 &format!("Max energy drift: {max_error:.2e}"),
350 )
351 }
352}
353
354#[must_use]
358pub fn test_angular_momentum_conservation(
359 initial_state: &NBodyState,
360 steps: usize,
361 dt: f64,
362 tolerance: f64,
363) -> MetamorphicResult {
364 let yoshida = YoshidaIntegrator::new();
365 let dt_orbit = OrbitTime::from_seconds(dt);
366
367 let initial_l = initial_state.angular_momentum_magnitude();
368 let mut state = initial_state.clone();
369 let mut max_error = 0.0_f64;
370
371 for _ in 0..steps {
372 if yoshida.step(&mut state, dt_orbit).is_err() {
373 return MetamorphicResult::fail(
374 "Angular Momentum Conservation",
375 f64::NAN,
376 tolerance,
377 "Simulation failed",
378 );
379 }
380
381 let current_l = state.angular_momentum_magnitude();
382 let rel_error = if initial_l.abs() > f64::EPSILON {
383 (current_l - initial_l).abs() / initial_l.abs()
384 } else {
385 (current_l - initial_l).abs()
386 };
387 max_error = max_error.max(rel_error);
388 }
389
390 if max_error <= tolerance {
391 MetamorphicResult::pass("Angular Momentum Conservation", max_error, tolerance)
392 } else {
393 MetamorphicResult::fail(
394 "Angular Momentum Conservation",
395 max_error,
396 tolerance,
397 &format!("Max L drift: {max_error:.2e}"),
398 )
399 }
400}
401
402#[must_use]
406pub fn test_deterministic_replay(
407 initial_state: &NBodyState,
408 steps: usize,
409 dt: f64,
410) -> MetamorphicResult {
411 let yoshida = YoshidaIntegrator::new();
412 let dt_orbit = OrbitTime::from_seconds(dt);
413
414 let mut state1 = initial_state.clone();
416 for _ in 0..steps {
417 if yoshida.step(&mut state1, dt_orbit).is_err() {
418 return MetamorphicResult::fail(
419 "Deterministic Replay",
420 f64::NAN,
421 0.0,
422 "First run failed",
423 );
424 }
425 }
426
427 let mut state2 = initial_state.clone();
429 for _ in 0..steps {
430 if yoshida.step(&mut state2, dt_orbit).is_err() {
431 return MetamorphicResult::fail(
432 "Deterministic Replay",
433 f64::NAN,
434 0.0,
435 "Second run failed",
436 );
437 }
438 }
439
440 #[allow(clippy::float_cmp)]
442 let identical = state1
443 .bodies
444 .iter()
445 .zip(state2.bodies.iter())
446 .all(|(b1, b2)| {
447 let (x1, y1, z1) = b1.position.as_meters();
448 let (x2, y2, z2) = b2.position.as_meters();
449 let (vx1, vy1, vz1) = b1.velocity.as_mps();
450 let (vx2, vy2, vz2) = b2.velocity.as_mps();
451
452 x1 == x2 && y1 == y2 && z1 == z2 && vx1 == vx2 && vy1 == vy2 && vz1 == vz2
453 });
454
455 if identical {
456 MetamorphicResult::pass("Deterministic Replay", 0.0, 0.0)
457 } else {
458 MetamorphicResult::fail(
459 "Deterministic Replay",
460 1.0,
461 0.0,
462 "Results not bit-identical",
463 )
464 }
465}
466
467#[must_use]
469pub fn run_all_metamorphic_tests(
470 state: &NBodyState,
471 steps: usize,
472 dt: f64,
473) -> Vec<MetamorphicResult> {
474 vec![
475 test_rotation_invariance(state, steps, dt, 1e-10),
476 test_time_reversal(state, steps, dt, 1e-6),
477 test_energy_conservation(state, steps, dt, 1e-9),
478 test_angular_momentum_conservation(state, steps, dt, 1e-12),
479 test_deterministic_replay(state, steps, dt),
480 ]
481}
482
483#[cfg(test)]
484mod tests {
485 use super::*;
486 use crate::orbit::scenarios::KeplerConfig;
487 use crate::orbit::units::AU;
488
489 fn create_earth_sun_state() -> NBodyState {
490 KeplerConfig::earth_sun().build(1e6)
491 }
492
493 #[test]
494 fn test_metamorphic_result_pass() {
495 let result = MetamorphicResult::pass("Test", 1e-10, 1e-9);
496 assert!(result.passed);
497 assert!(result.error < result.tolerance);
498 }
499
500 #[test]
501 fn test_metamorphic_result_fail() {
502 let result = MetamorphicResult::fail("Test", 1e-5, 1e-9, "Too large");
503 assert!(!result.passed);
504 assert!(result.error > result.tolerance);
505 }
506
507 #[test]
508 fn test_compute_pairwise_distances() {
509 let state = create_earth_sun_state();
510 let distances = compute_pairwise_distances(&state);
511 assert_eq!(distances.len(), 1); let perihelion = AU * (1.0 - 0.0167);
514 assert!((distances[0] - perihelion).abs() / perihelion < 0.01);
515 }
516
517 #[test]
518 fn test_rotate_z() {
519 let (x, y, z) = rotate_z(1.0, 0.0, 0.0, std::f64::consts::FRAC_PI_2);
520 assert!(x.abs() < 1e-10);
521 assert!((y - 1.0).abs() < 1e-10);
522 assert!(z.abs() < 1e-10);
523 }
524
525 #[test]
526 fn test_rotate_state() {
527 let state = create_earth_sun_state();
528 let rotated = rotate_state(&state, std::f64::consts::FRAC_PI_2);
529
530 let (sx, sy, _) = rotated.bodies[0].position.as_meters();
532 assert!(sx.abs() < 1e-10);
533 assert!(sy.abs() < 1e-10);
534
535 let (orig_x, _, _) = state.bodies[1].position.as_meters();
538 let (ex, ey, _) = rotated.bodies[1].position.as_meters();
539 assert!(ex.abs() < orig_x.abs() * 0.01); assert!((ey - orig_x).abs() / orig_x.abs() < 0.01); }
542
543 #[test]
544 fn test_reverse_velocities() {
545 let state = create_earth_sun_state();
546 let reversed = reverse_velocities(&state);
547
548 let (_, vy_orig, _) = state.bodies[1].velocity.as_mps();
549 let (_, vy_rev, _) = reversed.bodies[1].velocity.as_mps();
550
551 assert!((vy_orig + vy_rev).abs() < 1e-10);
552 }
553
554 #[test]
555 fn test_scale_masses() {
556 let state = create_earth_sun_state();
557 let scaled = scale_masses(&state, 2.0);
558
559 let orig_mass = state.bodies[0].mass.as_kg();
560 let scaled_mass = scaled.bodies[0].mass.as_kg();
561
562 assert!((scaled_mass - 2.0 * orig_mass).abs() / orig_mass < 1e-10);
563 }
564
565 #[test]
566 fn test_mr_rotation_invariance() {
567 let state = create_earth_sun_state();
568 let result = test_rotation_invariance(&state, 100, 3600.0, 1e-8);
569 assert!(result.passed, "Rotation invariance failed: {:?}", result);
570 }
571
572 #[test]
573 fn test_mr_time_reversal() {
574 let state = create_earth_sun_state();
575 let result = test_time_reversal(&state, 50, 3600.0, 1e-4);
578 assert!(result.passed, "Time-reversal failed: {:?}", result);
579 }
580
581 #[test]
582 fn test_mr_energy_conservation() {
583 let state = create_earth_sun_state();
584 let result = test_energy_conservation(&state, 100, 3600.0, 1e-9);
585 assert!(result.passed, "Energy conservation failed: {:?}", result);
586 }
587
588 #[test]
589 fn test_mr_angular_momentum_conservation() {
590 let state = create_earth_sun_state();
591 let result = test_angular_momentum_conservation(&state, 100, 3600.0, 1e-12);
592 assert!(
593 result.passed,
594 "Angular momentum conservation failed: {:?}",
595 result
596 );
597 }
598
599 #[test]
600 fn test_mr_deterministic_replay() {
601 let state = create_earth_sun_state();
602 let result = test_deterministic_replay(&state, 100, 3600.0);
603 assert!(result.passed, "Deterministic replay failed: {:?}", result);
604 }
605
606 #[test]
607 fn test_run_all_metamorphic_tests() {
608 let state = create_earth_sun_state();
609 let results = run_all_metamorphic_tests(&state, 50, 3600.0);
610
611 assert_eq!(results.len(), 5);
612
613 let deterministic = &results[4];
615 assert!(deterministic.passed, "Deterministic: {:?}", deterministic);
616
617 let angular = &results[3];
618 assert!(angular.passed, "Angular momentum: {:?}", angular);
619 }
620
621 #[test]
622 fn test_mr_energy_with_longer_simulation() {
623 let state = create_earth_sun_state();
624 let result = test_energy_conservation(&state, 2400, 3600.0, 1e-9);
627 assert!(result.passed, "Energy over 100 days: {:?}", result);
628 }
629
630 #[test]
631 fn test_orbital_period_metamorphic() {
632 let state = create_earth_sun_state();
634 let yoshida = YoshidaIntegrator::new();
635 let dt = OrbitTime::from_seconds(3600.0); let steps = 8766;
639 let mut current = state.clone();
640
641 for _ in 0..steps {
642 yoshida.step(&mut current, dt).expect("step failed");
643 }
644
645 let (x0, y0, _) = state.bodies[1].position.as_meters();
647 let (x1, y1, _) = current.bodies[1].position.as_meters();
648
649 let start_r = (x0 * x0 + y0 * y0).sqrt();
650 let pos_diff = ((x1 - x0).powi(2) + (y1 - y0).powi(2)).sqrt();
651 let rel_error = pos_diff / start_r;
652
653 assert!(rel_error < 0.01, "Orbital period error: {rel_error}");
655 }
656}