use std::ops::ControlFlow;
use crate::OrbitalState;
#[derive(Debug, Clone, PartialEq)]
pub enum SimulationEvent {
Collision {
altitude_km: f64,
},
AtmosphericEntry {
altitude_km: f64,
},
}
pub fn collision_check(
body_radius: f64,
atmosphere_altitude: Option<f64>,
) -> impl Fn(f64, &OrbitalState) -> ControlFlow<SimulationEvent> {
move |_t: f64, state: &OrbitalState| {
let r = state.position().magnitude();
if r < body_radius {
ControlFlow::Break(SimulationEvent::Collision {
altitude_km: r - body_radius,
})
} else if let Some(atm_alt) = atmosphere_altitude {
if r < body_radius + atm_alt {
ControlFlow::Break(SimulationEvent::AtmosphericEntry {
altitude_km: r - body_radius,
})
} else {
ControlFlow::Continue(())
}
} else {
ControlFlow::Continue(())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use arika::earth::{MU as MU_EARTH, R as R_EARTH};
use nalgebra::vector;
#[test]
fn collision_check_above_surface() {
let check = collision_check(R_EARTH, None);
let state = OrbitalState::new(vector![R_EARTH + 400.0, 0.0, 0.0], vector![0.0, 7.66, 0.0]);
assert!(matches!(check(0.0, &state), ControlFlow::Continue(())));
}
#[test]
fn collision_check_below_surface() {
let check = collision_check(R_EARTH, None);
let state = OrbitalState::new(vector![R_EARTH - 10.0, 0.0, 0.0], vector![0.0, 7.0, 0.0]);
match check(0.0, &state) {
ControlFlow::Break(SimulationEvent::Collision { altitude_km }) => {
assert!(altitude_km < 0.0);
assert!((altitude_km - (-10.0)).abs() < 1e-10);
}
other => panic!("Expected collision, got {other:?}"),
}
}
#[test]
fn collision_check_at_surface() {
let check = collision_check(R_EARTH, None);
let state = OrbitalState::new(vector![R_EARTH, 0.0, 0.0], vector![0.0, 7.0, 0.0]);
assert!(matches!(check(0.0, &state), ControlFlow::Continue(())));
}
#[test]
fn collision_check_3d_position() {
let check = collision_check(R_EARTH, None);
let state = OrbitalState::new(vector![3000.0, 3000.0, 3000.0], vector![0.0, 0.0, 0.0]);
assert!(matches!(
check(0.0, &state),
ControlFlow::Break(SimulationEvent::Collision { .. })
));
}
#[test]
fn atmospheric_entry_above_karman() {
let check = collision_check(R_EARTH, Some(100.0));
let state = OrbitalState::new(vector![R_EARTH + 200.0, 0.0, 0.0], vector![0.0, 7.66, 0.0]);
assert!(matches!(check(0.0, &state), ControlFlow::Continue(())));
}
#[test]
fn atmospheric_entry_below_karman() {
let check = collision_check(R_EARTH, Some(100.0));
let state = OrbitalState::new(vector![R_EARTH + 50.0, 0.0, 0.0], vector![0.0, 7.0, 0.0]);
match check(0.0, &state) {
ControlFlow::Break(SimulationEvent::AtmosphericEntry { altitude_km }) => {
assert!((altitude_km - 50.0).abs() < 1e-10);
}
other => panic!("Expected AtmosphericEntry, got {other:?}"),
}
}
#[test]
fn collision_takes_priority_over_atmospheric_entry() {
let check = collision_check(R_EARTH, Some(100.0));
let state = OrbitalState::new(vector![R_EARTH - 5.0, 0.0, 0.0], vector![0.0, 7.0, 0.0]);
assert!(matches!(
check(0.0, &state),
ControlFlow::Break(SimulationEvent::Collision { .. })
));
}
#[test]
fn suborbital_trajectory_terminates_on_collision() {
use crate::orbital::two_body::TwoBodySystem;
use utsuroi::{IntegrationOutcome, Integrator, Rk4};
let system = TwoBodySystem { mu: MU_EARTH };
let r = R_EARTH + 100.0;
let v_circular = (MU_EARTH / r).sqrt();
let initial = OrbitalState::new(vector![r, 0.0, 0.0], vector![0.0, v_circular * 0.8, 0.0]);
let event_checker = collision_check(R_EARTH, None);
let outcome = Rk4.integrate_with_events(
&system,
initial,
0.0,
100_000.0,
1.0,
|_t, _state| {},
event_checker,
);
match outcome {
IntegrationOutcome::Terminated {
reason: SimulationEvent::Collision { altitude_km },
t,
..
} => {
assert!(
altitude_km < 0.0,
"Altitude should be negative at collision"
);
assert!(t < 100_000.0, "Should terminate before t_end");
assert!(t > 0.0, "Should not terminate immediately");
}
other => panic!("Expected collision termination, got {other:?}"),
}
}
#[test]
fn suborbital_trajectory_terminates_on_atmospheric_entry() {
use crate::orbital::two_body::TwoBodySystem;
use utsuroi::{IntegrationOutcome, Integrator, Rk4};
let system = TwoBodySystem { mu: MU_EARTH };
let r = R_EARTH + 200.0;
let v_circular = (MU_EARTH / r).sqrt();
let initial = OrbitalState::new(vector![r, 0.0, 0.0], vector![0.0, v_circular * 0.8, 0.0]);
let event_checker = collision_check(R_EARTH, Some(100.0));
let outcome = Rk4.integrate_with_events(
&system,
initial,
0.0,
100_000.0,
1.0,
|_t, _state| {},
event_checker,
);
match outcome {
IntegrationOutcome::Terminated {
reason: SimulationEvent::AtmosphericEntry { altitude_km },
t,
..
} => {
assert!(altitude_km < 100.0, "Should be below atmosphere boundary");
assert!(altitude_km > 0.0, "Should still be above surface");
assert!(t > 0.0, "Should not terminate immediately");
}
other => panic!("Expected AtmosphericEntry, got {other:?}"),
}
}
}