pub const EARTH_MOON_MU: f64 = 0.012_150_585_609_624;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Cr3bpState {
pub r: [f64; 3],
pub v: [f64; 3],
}
pub fn cr3bp_accel(r: [f64; 3], v: [f64; 3], mu: f64) -> [f64; 3] {
let [x, y, z] = r;
let om = 1.0 - mu;
let r1c = ((x + mu).powi(2) + y * y + z * z).powf(1.5);
let r2c = ((x - om).powi(2) + y * y + z * z).powf(1.5);
[
2.0 * v[1] + x - om * (x + mu) / r1c - mu * (x - om) / r2c,
-2.0 * v[0] + y - om * y / r1c - mu * y / r2c,
-om * z / r1c - mu * z / r2c,
]
}
pub fn jacobi_constant(s: &Cr3bpState, mu: f64) -> f64 {
let [x, y, z] = s.r;
let om = 1.0 - mu;
let r1 = ((x + mu).powi(2) + y * y + z * z).sqrt();
let r2 = ((x - om).powi(2) + y * y + z * z).sqrt();
let u = 0.5 * (x * x + y * y) + om / r1 + mu / r2;
let v2 = s.v[0].powi(2) + s.v[1].powi(2) + s.v[2].powi(2);
2.0 * u - v2
}
fn deriv(s: Cr3bpState, mu: f64) -> Cr3bpState {
Cr3bpState {
r: s.v,
v: cr3bp_accel(s.r, s.v, mu),
}
}
fn axpy(a: Cr3bpState, sc: f64, b: Cr3bpState) -> Cr3bpState {
Cr3bpState {
r: [
a.r[0] + sc * b.r[0],
a.r[1] + sc * b.r[1],
a.r[2] + sc * b.r[2],
],
v: [
a.v[0] + sc * b.v[0],
a.v[1] + sc * b.v[1],
a.v[2] + sc * b.v[2],
],
}
}
pub fn propagate_cr3bp(s: Cr3bpState, mu: f64, dt: f64, steps: usize) -> Cr3bpState {
let n = steps.max(1);
let h = dt / n as f64;
let mut st = s;
for _ in 0..n {
let k1 = deriv(st, mu);
let k2 = deriv(axpy(st, h / 2.0, k1), mu);
let k3 = deriv(axpy(st, h / 2.0, k2), mu);
let k4 = deriv(axpy(st, h, k3), mu);
let mut next = st;
for j in 0..3 {
next.r[j] += h / 6.0 * (k1.r[j] + 2.0 * k2.r[j] + 2.0 * k3.r[j] + k4.r[j]);
next.v[j] += h / 6.0 * (k1.v[j] + 2.0 * k2.v[j] + 2.0 * k3.v[j] + k4.v[j]);
}
st = next;
}
st
}
pub fn lagrange_points(mu: f64) -> [[f64; 3]; 5] {
let om = 1.0 - mu;
let g = |x: f64| {
let r1 = (x + mu).abs();
let r2 = (x - om).abs();
x - om * (x + mu) / r1.powi(3) - mu * (x - om) / r2.powi(3)
};
let bisect = |lo: f64, hi: f64| -> f64 {
let (mut a, mut b) = (lo, hi);
let fa = g(a);
for _ in 0..200 {
let m = 0.5 * (a + b);
if fa * g(m) <= 0.0 {
b = m;
} else {
a = m;
}
}
0.5 * (a + b)
};
let l1 = bisect(0.5, om - 1e-6); let l2 = bisect(om + 1e-6, 2.0); let l3 = bisect(-1.5, -mu - 1e-6); let xeq = 0.5 - mu;
let yeq = 3.0_f64.sqrt() / 2.0;
[
[l1, 0.0, 0.0],
[l2, 0.0, 0.0],
[l3, 0.0, 0.0],
[xeq, yeq, 0.0],
[xeq, -yeq, 0.0],
]
}
#[cfg(test)]
mod tests {
use super::*;
fn norm(v: [f64; 3]) -> f64 {
(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt()
}
#[test]
fn lagrange_points_match_earth_moon_values() {
let l = lagrange_points(EARTH_MOON_MU);
assert!((l[0][0] - 0.836_915).abs() < 1e-4, "L1 x = {}", l[0][0]);
assert!((l[1][0] - 1.155_682).abs() < 1e-4, "L2 x = {}", l[1][0]);
assert!((l[2][0] - (-1.005_063)).abs() < 1e-4, "L3 x = {}", l[2][0]);
let xeq = 0.5 - EARTH_MOON_MU;
let yeq = 3.0_f64.sqrt() / 2.0;
assert!((l[3][0] - xeq).abs() < 1e-12 && (l[3][1] - yeq).abs() < 1e-12);
assert!((l[4][0] - xeq).abs() < 1e-12 && (l[4][1] + yeq).abs() < 1e-12);
}
#[test]
fn lagrange_points_are_equilibria() {
let l = lagrange_points(EARTH_MOON_MU);
for (i, &p) in l.iter().enumerate() {
let a = cr3bp_accel(p, [0.0; 3], EARTH_MOON_MU);
let tol = if i >= 3 { 1e-12 } else { 1e-7 };
assert!(
norm(a) < tol,
"L{} accel = {} (not an equilibrium)",
i + 1,
norm(a)
);
}
}
#[test]
fn jacobi_constant_is_conserved_under_propagation() {
let s0 = Cr3bpState {
r: [1.15, 0.0, -0.12],
v: [0.02, 0.18, 0.05],
};
let c0 = jacobi_constant(&s0, EARTH_MOON_MU);
let s1 = propagate_cr3bp(s0, EARTH_MOON_MU, 1.5, 15_000);
let c1 = jacobi_constant(&s1, EARTH_MOON_MU);
assert!((c1 - c0).abs() < 1e-7, "Jacobi drift {} (C0={c0})", c1 - c0);
assert!(norm([s1.r[0] - s0.r[0], s1.r[1] - s0.r[1], s1.r[2] - s0.r[2]]) > 1e-3);
}
#[test]
fn out_of_plane_acceleration_restores_toward_the_plane() {
let up = cr3bp_accel([0.9, 0.0, 0.1], [0.0; 3], EARTH_MOON_MU);
assert!(up[2] < 0.0, "z̈ above plane should be negative: {}", up[2]);
let down = cr3bp_accel([0.9, 0.0, -0.1], [0.0; 3], EARTH_MOON_MU);
assert!(
down[2] > 0.0,
"z̈ below plane should be positive: {}",
down[2]
);
}
}