use core::f64::consts::PI;
use crate::errors::LambertError;
use super::{LAMBERT_EPSILON, LambertInput, LambertSolution, MAX_ITERATIONS, TransferKind};
pub fn izzo(input: LambertInput, kind: TransferKind) -> Result<LambertSolution, LambertError> {
let r_init = input.initial_state.radius_km;
let r_final = input.final_state.radius_km;
let tof_s = (input.final_state.epoch - input.initial_state.epoch).to_seconds();
let mu_km3_s2 = input.mu_km2_s3();
let ri_norm = r_init.norm();
let rf_norm = r_final.norm();
let chord = r_final - r_init;
let c_norm = chord.norm();
let s = (ri_norm + rf_norm + c_norm) * 0.5;
let i_r1 = r_init / ri_norm;
let i_r2 = r_final / rf_norm;
let mut i_h = i_r1.cross(&i_r2);
i_h /= i_h.norm();
if i_h.z < 0.0 {
i_h = -i_h;
}
let lambda = 1.0 - c_norm / s;
let mut m_lambda = lambda.sqrt();
let revs = match kind {
TransferKind::NRevs(n) => n.into(),
_ => 0_u32,
};
let dm = if matches!(kind, TransferKind::NRevs(_)) {
1.0
} else {
kind.direction_of_motion(&r_final, &r_init)?
};
let retrograde = dm < 0.0;
let (mut i_t1, mut i_t2) = if retrograde {
m_lambda = -m_lambda;
(i_r1.cross(&i_h), i_r2.cross(&i_h))
} else {
(i_h.cross(&i_r1), i_h.cross(&i_r2))
};
i_t1 /= i_t1.norm();
i_t2 /= i_t2.norm();
let t = (2.0 * mu_km3_s2 / s.powi(3)).sqrt() * tof_s;
let (x, y) = find_xy(
m_lambda,
t,
revs,
MAX_ITERATIONS,
LAMBERT_EPSILON,
LAMBERT_EPSILON,
true,
)?;
let gamma = (mu_km3_s2 * s / 2.0).sqrt();
let rho = (ri_norm - rf_norm) / c_norm;
let sigma = (1.0 - rho.powi(2)).sqrt();
let (v_r1, v_r2, v_t1, v_t2) = reconstruct(x, y, ri_norm, rf_norm, m_lambda, gamma, rho, sigma);
let v_init = v_r1 * (r_init / ri_norm) + v_t1 * i_t1;
let v_final = v_r2 * (r_final / rf_norm) + v_t2 * i_t2;
Ok(LambertSolution {
v_init_km_s: v_init,
v_final_km_s: v_final,
phi_rad: 0.0,
input,
})
}
pub fn reconstruct(
x: f64,
y: f64,
r1: f64,
r2: f64,
ll: f64,
gamma: f64,
rho: f64,
sigma: f64,
) -> (f64, f64, f64, f64) {
let v_r1 = gamma * ((ll * y - x) - rho * (ll * y + x)) / r1;
let v_r2 = -gamma * ((ll * y - x) + rho * (ll * y + x)) / r2;
let v_t1 = gamma * sigma * (y + ll * x) / r1;
let v_t2 = gamma * sigma * (y + ll * x) / r2;
(v_r1, v_r2, v_t1, v_t2)
}
pub fn find_xy(
ll: f64,
t: f64,
m: u32,
maxiter: usize,
atol: f64,
rtol: f64,
low_path: bool,
) -> Result<(f64, f64), LambertError> {
assert!(ll.abs() < 1.0, "|ll| must be less than 1");
let mut m_max = (t / PI).floor() as u32;
let t_00 = ll.acos() + ll * (1.0 - ll.powi(2)).sqrt();
if t < t_00 + (m_max as f64) * PI && m_max > 0 {
let (_, t_min) = compute_t_min(ll, m_max, maxiter, atol, rtol)?;
if t < t_min {
m_max -= 1;
}
}
if m > m_max {
return Err(LambertError::MultiRevNotFeasible { m, m_max });
}
let x_0 = initial_guess(t, ll, m, low_path);
let x = householder(x_0, t, ll, m, atol, rtol, maxiter)?;
let y = compute_y(x, ll);
Ok((x, y))
}
fn compute_y(x: f64, ll: f64) -> f64 {
(1.0 - ll.powi(2) * (1.0 - x.powi(2))).sqrt()
}
fn compute_psi(x: f64, y: f64, ll: f64) -> f64 {
if x > 1.0 {
((y - x * ll) * (x.powi(2) - 1.0).sqrt()).asinh()
} else if x < 1.0 {
(x * y + ll * (1.0 - x.powi(2))).acos()
} else {
0.0
}
}
fn tof_equation(x: f64, t0: f64, ll: f64, m: u32) -> f64 {
let y = compute_y(x, ll);
tof_equation_y(x, y, t0, ll, m)
}
fn tof_equation_y(x: f64, y: f64, t0: f64, ll: f64, m: u32) -> f64 {
let t_ = if m == 0 && x.powi(2) > 0.6 && x.powi(2) < 1.4 {
let eta = y - ll * x;
let s_1 = (1.0 - ll - x * eta) * 0.5;
let q = 4.0 / 3.0 * hyp2f1b(s_1);
(eta.powi(3) * q + 4.0 * ll * eta) * 0.5
} else {
let psi = compute_psi(x, y, ll);
let m_float = m as f64;
let den = 1.0 - x.powi(2);
((psi + m_float * PI) / den.abs().sqrt() - x + ll * y) / den
};
t_ - t0
}
fn tof_equation_p(x: f64, y: f64, t: f64, ll: f64) -> f64 {
(3.0 * t * x - 2.0 + 2.0 * ll.powi(3) * x / y) / (1.0 - x.powi(2))
}
fn tof_equation_p2(x: f64, y: f64, t: f64, dt: f64, ll: f64) -> f64 {
(3.0 * t + 5.0 * x * dt + 2.0 * (1.0 - ll.powi(2)) * ll.powi(3) / y.powi(3)) / (1.0 - x.powi(2))
}
fn tof_equation_p3(x: f64, y: f64, _t: f64, dt: f64, ddt: f64, ll: f64) -> f64 {
(7.0 * x * ddt + 8.0 * dt - 6.0 * (1.0 - ll.powi(2)) * ll.powi(5) * x / y.powi(5))
/ (1.0 - x.powi(2))
}
fn compute_t_min(
ll: f64,
m: u32,
maxiter: usize,
atol: f64,
rtol: f64,
) -> Result<(f64, f64), LambertError> {
if (ll - 1.0).abs() < 1e-9 {
let x_t_min = 0.0;
let t_min = tof_equation(x_t_min, 0.0, ll, m);
Ok((x_t_min, t_min))
} else if m == 0 {
let x_t_min = f64::INFINITY;
let t_min = 0.0;
Ok((x_t_min, t_min))
} else {
let x_i = 0.1;
let t_i = tof_equation(x_i, 0.0, ll, m);
let x_t_min = halley(x_i, t_i, ll, atol, rtol, maxiter)?;
let t_min = tof_equation(x_t_min, 0.0, ll, m);
Ok((x_t_min, t_min))
}
}
fn initial_guess(tof_s: f64, ll: f64, m: u32, low_path: bool) -> f64 {
if m == 0 {
let t_0 = ll.acos() + ll * (1.0 - ll.powi(2)).sqrt(); let t_1 = 2.0 * (1.0 - ll.powi(3)) / 3.0;
if tof_s >= t_0 {
(t_0 / tof_s).powf(2.0 / 3.0) - 1.0
} else if tof_s < t_1 {
5.0 / 2.0 * t_1 / tof_s * (t_1 - tof_s) / (1.0 - ll.powi(5)) + 1.0
} else {
(2.0f64.ln() * (tof_s / t_0).ln() / (t_1 / t_0).ln()).exp() - 1.0
}
} else {
let m_float = m as f64;
let term_l = ((m_float * PI + PI) / (8.0 * tof_s)).powf(2.0 / 3.0);
let x_0l = (term_l - 1.0) / (term_l + 1.0);
let term_r = ((8.0 * tof_s) / (m_float * PI)).powf(2.0 / 3.0);
let x_0r = (term_r - 1.0) / (term_r + 1.0);
if low_path {
x_0l.max(x_0r)
} else {
x_0l.min(x_0r)
}
}
}
pub fn hyp2f1b(x: f64) -> f64 {
if x >= 1.0 {
return f64::INFINITY;
}
let mut res = 1.0;
let mut term = 1.0;
let mut ii = 0.0_f64; loop {
term *= (3.0 + ii) * (1.0 + ii) / (2.5 + ii) * x / (ii + 1.0);
let res_old = res;
res += term;
if res == res_old {
return res;
}
ii += 1.0;
}
}
pub fn halley(
mut p0: f64,
t0: f64,
ll: f64,
atol: f64,
rtol: f64,
maxiter: usize,
) -> Result<f64, LambertError> {
for _ in 1..=maxiter {
let y = compute_y(p0, ll);
let fder = tof_equation_p(p0, y, t0, ll);
let fder2 = tof_equation_p2(p0, y, t0, fder, ll);
if fder2.abs() < 1e-14 {
return Err(LambertError::TargetsTooClose);
}
let fder3 = tof_equation_p3(p0, y, t0, fder, fder2, ll);
let p = p0 - 2.0 * fder * fder2 / (2.0 * fder2.powi(2) - fder * fder3);
if (p - p0).abs() < rtol * p0.abs() + atol {
return Ok(p);
}
p0 = p;
}
Err(LambertError::SolverMaxIter { maxiter })
}
pub fn householder(
mut p0: f64,
t0: f64,
ll: f64,
m: u32,
atol: f64,
rtol: f64,
maxiter: usize,
) -> Result<f64, LambertError> {
for _ in 1..=maxiter {
let y = compute_y(p0, ll);
let fval = tof_equation_y(p0, y, t0, ll, m);
let t = fval + t0;
let fder = tof_equation_p(p0, y, t, ll);
let fder2 = tof_equation_p2(p0, y, t, fder, ll);
let fder3 = tof_equation_p3(p0, y, t, fder, fder2, ll);
let num = fder.powi(2) - fval * fder2 / 2.0;
let den = fder * (fder.powi(2) - fval * fder2) + fder3 * fval.powi(2) / 6.0;
if den.abs() < 1e-14 {
return Err(LambertError::TargetsTooClose);
}
let p = p0 - fval * (num / den);
if (p - p0).abs() < rtol * p0.abs() + atol {
return Ok(p);
}
p0 = p;
}
Err(LambertError::SolverMaxIter { maxiter })
}
#[cfg(test)]
mod ut_lambert_izzo {
use super::{LambertInput, TransferKind, izzo};
use crate::linalg::Vector3;
use anise::prelude::{Frame, Orbit};
use hifitime::{Epoch, Unit};
#[test]
fn test_lambert_izzo_shortway() {
let frame = Frame {
ephemeris_id: 301,
orientation_id: 0,
mu_km3_s2: Some(3.98600433e5),
shape: None,
frozen_epoch: None,
force_inertial: false,
};
let initial_state = Orbit {
radius_km: Vector3::new(15945.34, 0.0, 0.0),
velocity_km_s: Vector3::zeros(),
epoch: Epoch::from_gregorian_utc_at_midnight(2025, 1, 1),
frame,
};
let final_state = Orbit {
radius_km: Vector3::new(12214.83899, 10249.46731, 0.0),
velocity_km_s: Vector3::zeros(),
epoch: Epoch::from_gregorian_utc_at_midnight(2025, 1, 1) + Unit::Minute * 76.0,
frame,
};
let input = LambertInput::from_planetary_states(initial_state, final_state).unwrap();
let exp_vi = Vector3::new(2.058913, 2.915965, 0.0);
let exp_vf = Vector3::new(-3.451565, 0.910315, 0.0);
let sol = izzo(input, TransferKind::ShortWay).unwrap();
println!("{sol:?}\t{exp_vi}\t{exp_vf}");
assert!((sol.v_init_km_s - exp_vi).norm() < 1e-5);
assert!((sol.v_final_km_s - exp_vf).norm() < 1e-5);
assert!(
sol.v_inf_outgoing_declination_deg().abs() < 1e-8,
"outgoing decl should be zero"
);
assert!(
(sol.v_inf_outgoing_right_ascension_deg() - 54.7747288).abs() < 1e-6,
"wrong outgoing RA"
);
}
#[test]
fn test_lambert_izzo_longway() {
let frame = Frame {
ephemeris_id: 301,
orientation_id: 0,
mu_km3_s2: Some(3.98600433e5),
shape: None,
frozen_epoch: None,
force_inertial: false,
};
let initial_state = Orbit {
radius_km: Vector3::new(15945.34, 0.0, 0.0),
velocity_km_s: Vector3::zeros(),
epoch: Epoch::from_gregorian_utc_at_midnight(2025, 1, 1),
frame,
};
let final_state = Orbit {
radius_km: Vector3::new(12214.83899, 10249.46731, 0.0),
velocity_km_s: Vector3::zeros(),
epoch: Epoch::from_gregorian_utc_at_midnight(2025, 1, 1) + Unit::Minute * 76.0,
frame,
};
let input = LambertInput::from_planetary_states(initial_state, final_state).unwrap();
let exp_vi = Vector3::new(-3.811158, -2.003854, 0.0);
let exp_vf = Vector3::new(4.207569, 0.914724, 0.0);
let sol = izzo(input, TransferKind::LongWay).unwrap();
assert!((sol.v_init_km_s - exp_vi).norm() < 1e-5);
assert!((sol.v_final_km_s - exp_vf).norm() < 1e-5);
assert!(
sol.v_inf_outgoing_declination_deg().abs() < 1e-8,
"outgoing decl should be zero"
);
assert!(
(sol.v_inf_outgoing_right_ascension_deg() + 152.2652291).abs() < 1e-6,
"wrong outgoing RA"
);
}
#[test]
fn test_lambert_izzo_direction_toggle_ih_negative() {
let frame = Frame {
ephemeris_id: 301,
orientation_id: 0,
mu_km3_s2: Some(3.98600433e5),
shape: None,
frozen_epoch: None,
force_inertial: false,
};
let t0 = Epoch::from_gregorian_utc_at_midnight(2025, 1, 1);
let r1 = Vector3::new(7000.0, 0.0, 0.0);
let r2 = Vector3::new(0.0, -7000.0, -500.0);
let initial = Orbit {
radius_km: r1,
velocity_km_s: Vector3::zeros(),
epoch: t0,
frame,
};
let final_ = Orbit {
radius_km: r2,
velocity_km_s: Vector3::zeros(),
epoch: t0 + Unit::Minute * 60.0,
frame,
};
let input = LambertInput::from_planetary_states(initial, final_).unwrap();
let short = izzo(input, TransferKind::ShortWay).unwrap();
let long = izzo(input, TransferKind::LongWay).unwrap();
assert!(
(short.v_init_km_s - long.v_init_km_s).norm() > 1e-3,
"direction toggle ignored — short and long-way collapsed: short={:?}, long={:?}",
short.v_init_km_s,
long.v_init_km_s,
);
}
#[test]
fn test_lambert_izzo_auto_matches_longway_for_long_arc() {
let frame = Frame {
ephemeris_id: 301,
orientation_id: 0,
mu_km3_s2: Some(3.98600433e5),
shape: None,
frozen_epoch: None,
force_inertial: false,
};
let t0 = Epoch::from_gregorian_utc_at_midnight(2025, 1, 1);
let initial = Orbit {
radius_km: Vector3::new(8000.0, 0.0, 0.0),
velocity_km_s: Vector3::zeros(),
epoch: t0,
frame,
};
let final_ = Orbit {
radius_km: Vector3::new(0.0, -8000.0, 0.0),
velocity_km_s: Vector3::zeros(),
epoch: t0 + Unit::Minute * 60.0,
frame,
};
let input = LambertInput::from_planetary_states(initial, final_).unwrap();
let auto = izzo(input, TransferKind::Auto).unwrap();
let long = izzo(input, TransferKind::LongWay).unwrap();
let short = izzo(input, TransferKind::ShortWay).unwrap();
assert!(
(auto.v_init_km_s - long.v_init_km_s).norm() < 1e-10,
"Auto should match LongWay for long-arc geometry",
);
assert!(
(auto.v_init_km_s - short.v_init_km_s).norm() > 1e-3,
"Auto should differ from ShortWay for long-arc geometry",
);
}
#[test]
fn test_lambert_izzo_auto_matches_shortway_for_short_arc() {
let frame = Frame {
ephemeris_id: 301,
orientation_id: 0,
mu_km3_s2: Some(3.98600433e5),
shape: None,
frozen_epoch: None,
force_inertial: false,
};
let t0 = Epoch::from_gregorian_utc_at_midnight(2025, 1, 1);
let initial = Orbit {
radius_km: Vector3::new(8000.0, 0.0, 0.0),
velocity_km_s: Vector3::zeros(),
epoch: t0,
frame,
};
let final_ = Orbit {
radius_km: Vector3::new(0.0, 8000.0, 0.0),
velocity_km_s: Vector3::zeros(),
epoch: t0 + Unit::Minute * 60.0,
frame,
};
let input = LambertInput::from_planetary_states(initial, final_).unwrap();
let auto = izzo(input, TransferKind::Auto).unwrap();
let short = izzo(input, TransferKind::ShortWay).unwrap();
let long = izzo(input, TransferKind::LongWay).unwrap();
assert!(
(auto.v_init_km_s - short.v_init_km_s).norm() < 1e-10,
"Auto should match ShortWay for short-arc geometry",
);
assert!(
(auto.v_init_km_s - long.v_init_km_s).norm() > 1e-3,
"Auto should differ from LongWay for short-arc geometry",
);
}
}