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],
]
}
pub const EARTH_MOON_DIST_KM: f64 = 384_400.0;
pub const SIDEREAL_MONTH_DAYS: f64 = 27.321_661;
pub const MOON_RADIUS_KM: f64 = 1_737.4;
pub fn cr3bp_jacobian(r: [f64; 3], mu: f64) -> [[f64; 6]; 6] {
let [x, y, z] = r;
let a = 1.0 - mu;
let b = mu;
let dx1 = x + mu;
let dx2 = x - 1.0 + mu;
let r1 = (dx1 * dx1 + y * y + z * z).sqrt();
let r2 = (dx2 * dx2 + y * y + z * z).sqrt();
let (r1_3, r2_3) = (r1.powi(3), r2.powi(3));
let (r1_5, r2_5) = (r1.powi(5), r2.powi(5));
let oxx = 1.0 - a / r1_3 - b / r2_3 + 3.0 * a * dx1 * dx1 / r1_5 + 3.0 * b * dx2 * dx2 / r2_5;
let oyy = 1.0 - a / r1_3 - b / r2_3 + 3.0 * a * y * y / r1_5 + 3.0 * b * y * y / r2_5;
let ozz = -a / r1_3 - b / r2_3 + 3.0 * a * z * z / r1_5 + 3.0 * b * z * z / r2_5;
let oxy = 3.0 * a * dx1 * y / r1_5 + 3.0 * b * dx2 * y / r2_5;
let oxz = 3.0 * a * dx1 * z / r1_5 + 3.0 * b * dx2 * z / r2_5;
let oyz = 3.0 * a * y * z / r1_5 + 3.0 * b * y * z / r2_5;
let mut m = [[0.0f64; 6]; 6];
m[0][3] = 1.0;
m[1][4] = 1.0;
m[2][5] = 1.0;
m[3][0] = oxx;
m[3][1] = oxy;
m[3][2] = oxz;
m[3][4] = 2.0; m[4][0] = oxy;
m[4][1] = oyy;
m[4][2] = oyz;
m[4][3] = -2.0; m[5][0] = oxz;
m[5][1] = oyz;
m[5][2] = ozz;
m
}
fn state_to_vec(s: &Cr3bpState) -> [f64; 6] {
[s.r[0], s.r[1], s.r[2], s.v[0], s.v[1], s.v[2]]
}
fn vec_to_state(v: &[f64; 6]) -> Cr3bpState {
Cr3bpState {
r: [v[0], v[1], v[2]],
v: [v[3], v[4], v[5]],
}
}
fn identity6() -> [[f64; 6]; 6] {
let mut m = [[0.0f64; 6]; 6];
for (i, row) in m.iter_mut().enumerate() {
row[i] = 1.0;
}
m
}
fn matmul6(a: &[[f64; 6]; 6], b: &[[f64; 6]; 6]) -> [[f64; 6]; 6] {
let mut c = [[0.0f64; 6]; 6];
for i in 0..6 {
for j in 0..6 {
let mut s = 0.0;
for (k, brow) in b.iter().enumerate() {
s += a[i][k] * brow[j];
}
c[i][j] = s;
}
}
c
}
fn deriv_with_stm(x: &[f64; 6], phi: &[[f64; 6]; 6], mu: f64) -> ([f64; 6], [[f64; 6]; 6]) {
let acc = cr3bp_accel([x[0], x[1], x[2]], [x[3], x[4], x[5]], mu);
let dx = [x[3], x[4], x[5], acc[0], acc[1], acc[2]];
let a = cr3bp_jacobian([x[0], x[1], x[2]], mu);
(dx, matmul6(&a, phi))
}
pub fn propagate_state_stm(
s0: &Cr3bpState,
mu: f64,
t: f64,
steps: usize,
) -> (Cr3bpState, [[f64; 6]; 6]) {
let n = steps.max(1);
let h = t / n as f64;
let mut x = state_to_vec(s0);
let mut phi = identity6();
let lin = |x: &[f64; 6], p: &[[f64; 6]; 6], kx: &[f64; 6], kp: &[[f64; 6]; 6], sc: f64| {
let mut xo = *x;
let mut po = *p;
for i in 0..6 {
xo[i] += sc * kx[i];
for j in 0..6 {
po[i][j] += sc * kp[i][j];
}
}
(xo, po)
};
for _ in 0..n {
let (k1x, k1p) = deriv_with_stm(&x, &phi, mu);
let (x2, p2) = lin(&x, &phi, &k1x, &k1p, h / 2.0);
let (k2x, k2p) = deriv_with_stm(&x2, &p2, mu);
let (x3, p3) = lin(&x, &phi, &k2x, &k2p, h / 2.0);
let (k3x, k3p) = deriv_with_stm(&x3, &p3, mu);
let (x4, p4) = lin(&x, &phi, &k3x, &k3p, h);
let (k4x, k4p) = deriv_with_stm(&x4, &p4, mu);
for i in 0..6 {
x[i] += h / 6.0 * (k1x[i] + 2.0 * k2x[i] + 2.0 * k3x[i] + k4x[i]);
for j in 0..6 {
phi[i][j] += h / 6.0 * (k1p[i][j] + 2.0 * k2p[i][j] + 2.0 * k3p[i][j] + k4p[i][j]);
}
}
}
(vec_to_state(&x), phi)
}
fn propagate_to_crossing(
s0: &Cr3bpState,
mu: f64,
t_min: f64,
t_max: f64,
) -> Option<(Cr3bpState, [[f64; 6]; 6], f64)> {
let n = 6000;
let h = t_max / n as f64;
let mut x = state_to_vec(s0);
let mut phi = identity6();
let mut t = 0.0;
for _ in 0..n {
let x_prev = x;
let phi_prev = phi;
let t_prev = t;
let (s1, p1) = propagate_state_stm(&vec_to_state(&x), mu, h, 1);
x = state_to_vec(&s1);
phi = matmul6(&p1, &phi);
t += h;
if t_prev > t_min && x_prev[1] * x[1] < 0.0 {
let mut dt = -x_prev[1] / x_prev[4];
let mut sc = vec_to_state(&x_prev);
let mut phi_sub = identity6();
for _ in 0..8 {
let (s2, p2) = propagate_state_stm(&vec_to_state(&x_prev), mu, dt, 400);
sc = s2;
phi_sub = p2;
if sc.r[1].abs() < 1e-13 {
break;
}
dt -= sc.r[1] / sc.v[1];
}
return Some((sc, matmul6(&phi_sub, &phi_prev), t_prev + dt));
}
}
None
}
#[derive(Clone, Copy, Debug)]
pub struct PeriodicOrbit {
pub ic: Cr3bpState,
pub period: f64,
pub jacobi: f64,
}
impl PeriodicOrbit {
pub fn period_days(&self) -> f64 {
self.period * SIDEREAL_MONTH_DAYS / (2.0 * std::f64::consts::PI)
}
pub fn perilune_radius_km(&self, mu: f64, samples: usize) -> f64 {
let n = samples.max(100);
let mut min_d = f64::INFINITY;
for i in 0..=n {
let frac = i as f64 / n as f64;
let s = propagate_cr3bp(
self.ic,
mu,
self.period * frac,
(4000.0 * frac) as usize + 1,
);
let dx = s.r[0] - (1.0 - mu);
let d = (dx * dx + s.r[1] * s.r[1] + s.r[2] * s.r[2]).sqrt();
min_d = min_d.min(d);
}
min_d * EARTH_MOON_DIST_KM
}
}
pub fn differential_correct_halo(
guess: &Cr3bpState,
mu: f64,
tol: f64,
max_iter: usize,
) -> Option<PeriodicOrbit> {
let x0 = guess.r[0]; let mut z0 = guess.r[2];
let mut vy0 = guess.v[1];
for _ in 0..max_iter {
let s0 = Cr3bpState {
r: [x0, 0.0, z0],
v: [0.0, vy0, 0.0],
};
let (sc, phi, t_h) = propagate_to_crossing(&s0, mu, 0.1, 7.0)?;
let (vxf, vyf, vzf) = (sc.v[0], sc.v[1], sc.v[2]);
if vxf.hypot(vzf) < tol {
return Some(PeriodicOrbit {
ic: s0,
period: 2.0 * t_h,
jacobi: jacobi_constant(&s0, mu),
});
}
let acc = cr3bp_accel(sc.r, sc.v, mu);
let (axf, azf) = (acc[0], acc[2]);
let m11 = phi[3][2] - axf * phi[1][2] / vyf;
let m12 = phi[3][4] - axf * phi[1][4] / vyf;
let m21 = phi[5][2] - azf * phi[1][2] / vyf;
let m22 = phi[5][4] - azf * phi[1][4] / vyf;
let det = m11 * m22 - m12 * m21;
if det.abs() < 1e-14 {
return None;
}
let (b1, b2) = (-vxf, -vzf);
let dz = (b1 * m22 - m12 * b2) / det;
let dvy = (m11 * b2 - b1 * m21) / det;
z0 += dz;
vy0 += dvy;
}
None
}
#[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]
);
}
#[test]
fn stm_matches_finite_difference() {
let s = Cr3bpState {
r: [1.05, 0.02, -0.10],
v: [0.10, 0.20, -0.05],
};
let (t, steps) = (0.30, 4000);
let (_, phi) = propagate_state_stm(&s, EARTH_MOON_MU, t, steps);
let base = super::state_to_vec(&s);
let eps = 1e-6;
for j in 0..6 {
let mut sp = base;
let mut sm = base;
sp[j] += eps;
sm[j] -= eps;
let (ep, _) = propagate_state_stm(&super::vec_to_state(&sp), EARTH_MOON_MU, t, steps);
let (em, _) = propagate_state_stm(&super::vec_to_state(&sm), EARTH_MOON_MU, t, steps);
let ev = super::state_to_vec(&ep);
let mv = super::state_to_vec(&em);
for i in 0..6 {
let fd = (ev[i] - mv[i]) / (2.0 * eps);
assert!(
(phi[i][j] - fd).abs() < 1e-5,
"STM[{i}][{j}]={} vs finite-diff {fd}",
phi[i][j]
);
}
}
}
#[test]
fn differential_corrector_produces_periodic_halo() {
let mu = EARTH_MOON_MU;
let guess = Cr3bpState {
r: [1.08, 0.0, -0.10],
v: [0.0, -0.10, 0.0],
};
let orbit = differential_correct_halo(&guess, mu, 1e-11, 60)
.expect("L2 halo differential correction should converge");
assert!(orbit.ic.r[2].abs() > 0.05, "halo should be out of plane");
let end = propagate_cr3bp(orbit.ic, mu, orbit.period, 80_000);
let close = norm([
end.r[0] - orbit.ic.r[0],
end.r[1] - orbit.ic.r[1],
end.r[2] - orbit.ic.r[2],
]);
assert!(
close < 1e-6,
"halo should close on itself, residual {close:.2e}"
);
let cj = jacobi_constant(&end, mu);
assert!(
(cj - orbit.jacobi).abs() < 1e-7,
"Jacobi drift over a period"
);
}
#[test]
fn reproduces_l2_southern_nrho_92_regime() {
let mu = EARTH_MOON_MU;
let guess = Cr3bpState {
r: [1.0220, 0.0, -0.1800],
v: [0.0, -0.1020, 0.0],
};
let nrho = differential_correct_halo(&guess, mu, 1e-11, 80)
.expect("9:2 NRHO differential correction should converge");
assert!(
nrho.ic.r[2].abs() > 0.15,
"NRHO apolune amplitude too small"
);
let days = nrho.period_days();
assert!(
(6.0..=7.2).contains(&days),
"NRHO period {days:.3} d outside 9:2 regime"
);
let peri = nrho.perilune_radius_km(mu, 600);
assert!(
(2_500.0..=5_000.0).contains(&peri),
"NRHO perilune {peri:.0} km outside the near-rectilinear regime"
);
assert!(
peri > MOON_RADIUS_KM,
"perilune must clear the lunar surface"
);
let end = propagate_cr3bp(nrho.ic, mu, nrho.period, 120_000);
let close = norm([
end.r[0] - nrho.ic.r[0],
end.r[1] - nrho.ic.r[1],
end.r[2] - nrho.ic.r[2],
]);
assert!(
close < 1e-4,
"NRHO should close on itself, residual {close:.2e}"
);
}
}