use nalgebra::Vector3;
use super::bulirsch_stoer::{bs_step, changeover_k};
use super::forces::kick_bodies;
use super::kepler::kepler_drift;
use super::types::{MassiveBody, SimConfig, StateVector};
use super::GM_SUN;
pub fn changeover_radii(bodies: &[MassiveBody], changeover_hill: f64) -> Vec<f64> {
bodies
.iter()
.map(|body| {
let r_planet = body.state.pos.norm();
changeover_hill * r_planet * (body.mass / 3.0).cbrt()
})
.collect()
}
fn min_separation(particle: &StateVector, body: &MassiveBody, dt: f64) -> f64 {
let dr = particle.pos - body.state.pos;
let dv = particle.vel - body.state.vel;
let dv2 = dv.norm_squared();
let s = if dv2 > 1e-30 {
(-dr.dot(&dv) / dv2).clamp(0.0, dt)
} else {
0.0
};
(dr + s * dv).norm()
}
pub fn find_close_encounters(
particles: &[StateVector],
active: &[bool],
bodies: &[MassiveBody],
r_crit: &[f64],
dt: f64,
) -> Vec<bool> {
let mut needs_bs = vec![false; particles.len()];
for (i, particle) in particles.iter().enumerate() {
if !active[i] {
continue;
}
for (body, &rc) in bodies.iter().zip(r_crit) {
if min_separation(particle, body, dt) < rc {
needs_bs[i] = true;
break;
}
}
}
needs_bs
}
fn kick_particles_changeover(
particles: &mut [StateVector],
active: &[bool],
bodies: &[MassiveBody],
r_crit: &[f64],
dt: f64,
) {
for (i, particle) in particles.iter_mut().enumerate() {
if !active[i] {
continue;
}
let mut accel = Vector3::zeros();
for (body, &rc) in bodies.iter().zip(r_crit) {
let dr = particle.pos - body.state.pos;
let dr_mag = dr.norm();
let rp = body.state.pos.norm();
if dr_mag > 1e-30 {
accel -= changeover_k(dr_mag, rc) * body.gm * dr / dr_mag.powi(3);
}
if rp > 1e-30 {
accel -= body.gm * body.state.pos / rp.powi(3);
}
}
particle.vel += dt * accel;
}
}
pub fn hybrid_step(
bodies: &mut [MassiveBody],
particles: &mut [StateVector],
active: &mut [bool],
dt: f64,
config: &SimConfig,
) -> usize {
let half_dt = 0.5 * dt;
let r_crit = changeover_radii(bodies, config.hybrid_changeover_hill);
let needs_bs = find_close_encounters(particles, active, bodies, &r_crit, dt);
kick_bodies(bodies, half_dt);
kick_particles_changeover(particles, active, bodies, &r_crit, half_dt);
let bodies_at_start: Vec<MassiveBody> = bodies.to_vec();
for body in bodies.iter_mut() {
let gm_total = GM_SUN + body.gm;
body.state = kepler_drift(&body.state, dt, gm_total);
}
let mut failed = 0;
for (i, particle) in particles.iter_mut().enumerate() {
if !active[i] {
continue;
}
if needs_bs[i] {
let (new_state, converged) =
bs_step(particle, &bodies_at_start, &r_crit, dt, config.bs_epsilon);
if !converged {
failed += 1;
}
*particle = new_state;
} else {
*particle = kepler_drift(particle, dt, GM_SUN);
}
}
kick_bodies(bodies, half_dt);
kick_particles_changeover(particles, active, bodies, &r_crit, half_dt);
for (i, particle) in particles.iter().enumerate() {
if !active[i] {
continue;
}
let r = particle.pos.norm();
if r < config.removal_inner_au || r > config.removal_outer_au {
active[i] = false;
}
}
failed
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
fn test_config(dt: f64) -> SimConfig {
SimConfig {
dt,
removal_inner_au: 0.01,
removal_outer_au: 1e6,
hybrid_changeover_hill: 3.0,
bs_epsilon: 1e-12,
}
}
fn jupiter() -> MassiveBody {
let gm = 1e-3 * GM_SUN;
let a = 5.2;
let v = (GM_SUN / a).sqrt();
MassiveBody::new(
"Jupiter",
gm,
StateVector::new(Vector3::new(a, 0.0, 0.0), Vector3::new(0.0, v, 0.0)),
)
}
#[test]
fn test_encounter_detected_mid_step() {
let jup = jupiter();
let r_crit = changeover_radii(std::slice::from_ref(&jup), 3.0);
assert!(r_crit[0] > 1.0 && r_crit[0] < 1.2, "r_crit = {}", r_crit[0]);
let dt = 300.0;
let particle = StateVector::new(
Vector3::new(5.2, -3.0, 0.0),
jup.state.vel + Vector3::new(0.0, 0.02, 0.0),
);
let flagged = find_close_encounters(
&[particle],
&[true],
std::slice::from_ref(&jup),
&r_crit,
dt,
);
assert!(flagged[0], "mid-step crossing must be flagged");
let d_start = (particle.pos - jup.state.pos).norm();
assert!(d_start > r_crit[0]);
}
#[test]
fn test_hybrid_matches_kepler_far_from_planets() {
let mut bodies = vec![jupiter()];
let a = 60.0;
let v = (GM_SUN / a).sqrt();
let p0 = StateVector::new(Vector3::new(a, 0.0, 0.0), Vector3::new(0.0, v, 0.0));
let mut particles = vec![p0];
let mut active = vec![true];
let config = test_config(100.0);
for _ in 0..100 {
let failed = hybrid_step(&mut bodies, &mut particles, &mut active, config.dt, &config);
assert_eq!(failed, 0);
}
let r = particles[0].pos.norm();
assert_relative_eq!(r, a, epsilon = 0.05);
}
#[test]
fn test_hybrid_energy_through_encounter() {
let mut bodies = vec![jupiter()];
let q = 5.0;
let a = 20.0;
let v_peri = (GM_SUN * (2.0 / q - 1.0 / a)).sqrt();
let p0 = StateVector::new(Vector3::new(q, 0.0, 0.0), Vector3::new(0.0, v_peri, 0.0));
let mut particles = vec![p0];
let mut active = vec![true];
let config = test_config(150.0);
let mut total_failed = 0;
for _ in 0..500 {
total_failed +=
hybrid_step(&mut bodies, &mut particles, &mut active, config.dt, &config);
}
assert_eq!(total_failed, 0, "BS should converge through encounters");
assert!(active[0], "particle should survive");
let r = particles[0].pos.norm();
assert!(r.is_finite() && r > 0.1 && r < 1e4, "r = {r}");
}
}