1use core::f64::consts::PI;
20
21use crate::errors::LambertError;
22
23use super::{LambertInput, LambertSolution, TransferKind, LAMBERT_EPSILON, MAX_ITERATIONS};
24
25pub fn izzo(input: LambertInput, kind: TransferKind) -> Result<LambertSolution, LambertError> {
45 let r_init = input.initial_state.radius_km;
46 let r_final = input.final_state.radius_km;
47 let tof_s = (input.final_state.epoch - input.initial_state.epoch).to_seconds();
48 let mu_km3_s2 = input.mu_km2_s3();
49
50 let ri_norm = r_init.norm();
51 let rf_norm = r_final.norm();
52
53 let chord = r_final - r_init;
54 let c_norm = chord.norm();
55
56 let s = (ri_norm + rf_norm + c_norm) * 0.5;
58
59 let i_r1 = r_init / ri_norm;
61 let i_r2 = r_final / rf_norm;
62
63 let mut i_h = i_r1.cross(&i_r2);
64 i_h /= i_h.norm(); let lambda = 1.0 - c_norm / s;
68 let mut m_lambda = lambda.sqrt();
69
70 let retrograde = matches!(kind, TransferKind::LongWay);
71
72 let (mut i_t1, mut i_t2) = if i_h.z < 0.0 || retrograde {
73 m_lambda = -m_lambda;
75 (i_r1.cross(&i_h), i_r2.cross(&i_h))
76 } else {
77 (i_h.cross(&i_r1), i_h.cross(&i_r2))
78 };
79 i_t1 /= i_t1.norm();
81 i_t2 /= i_t2.norm();
82
83 let t = (2.0 * mu_km3_s2 / s.powi(3)).sqrt() * tof_s;
85
86 let (x, y) = find_xy(
88 m_lambda,
89 t,
90 match kind {
91 TransferKind::NRevs(revs) => revs.into(),
92 _ => 0,
93 },
94 MAX_ITERATIONS,
95 LAMBERT_EPSILON,
96 LAMBERT_EPSILON,
97 true,
98 )?;
99 let gamma = (mu_km3_s2 * s / 2.0).sqrt();
101 let rho = (ri_norm - rf_norm) / c_norm;
102 let sigma = (1.0 - rho.powi(2)).sqrt();
103
104 let (v_r1, v_r2, v_t1, v_t2) = reconstruct(x, y, ri_norm, rf_norm, m_lambda, gamma, rho, sigma);
107
108 let v_init = v_r1 * (r_init / ri_norm) + v_t1 * i_t1;
110 let v_final = v_r2 * (r_final / rf_norm) + v_t2 * i_t2;
111
112 Ok(LambertSolution {
113 v_init_km_s: v_init,
114 v_final_km_s: v_final,
115 phi_rad: 0.0,
116 input,
117 })
118}
119
120pub fn reconstruct(
123 x: f64,
124 y: f64,
125 r1: f64,
126 r2: f64,
127 ll: f64,
128 gamma: f64,
129 rho: f64,
130 sigma: f64,
131) -> (f64, f64, f64, f64) {
132 let v_r1 = gamma * ((ll * y - x) - rho * (ll * y + x)) / r1;
133 let v_r2 = -gamma * ((ll * y - x) + rho * (ll * y + x)) / r2;
134 let v_t1 = gamma * sigma * (y + ll * x) / r1;
135 let v_t2 = gamma * sigma * (y + ll * x) / r2;
136
137 (v_r1, v_r2, v_t1, v_t2)
138}
139
140pub fn find_xy(
146 ll: f64,
147 t: f64,
148 m: u32,
149 maxiter: usize,
150 atol: f64,
151 rtol: f64,
152 low_path: bool,
153) -> Result<(f64, f64), LambertError> {
154 assert!(ll.abs() < 1.0, "|ll| must be less than 1");
157
158 let mut m_max = (t / PI).floor() as u32;
159 let t_00 = ll.acos() + ll * (1.0 - ll.powi(2)).sqrt(); if t < t_00 + (m_max as f64) * PI && m_max > 0 {
163 let (_, t_min) = compute_t_min(ll, m_max, maxiter, atol, rtol)?;
164 if t < t_min {
165 m_max -= 1;
166 }
167 }
168
169 if m > m_max {
171 return Err(LambertError::MultiRevNotFeasible { m, m_max });
172 }
173
174 let x_0 = initial_guess(t, ll, m, low_path);
176
177 let x = householder(x_0, t, ll, m, atol, rtol, maxiter)?;
180 let y = compute_y(x, ll);
181
182 Ok((x, y))
183}
184
185fn compute_y(x: f64, ll: f64) -> f64 {
186 (1.0 - ll.powi(2) * (1.0 - x.powi(2))).sqrt()
187}
188
189fn compute_psi(x: f64, y: f64, ll: f64) -> f64 {
193 if x > 1.0 {
194 ((y - x * ll) * (x.powi(2) - 1.0).sqrt()).asinh()
196 } else if x < 1.0 {
197 (x * y + ll * (1.0 - x.powi(2))).acos()
200 } else {
201 0.0
203 }
204}
205
206fn tof_equation(x: f64, t0: f64, ll: f64, m: u32) -> f64 {
208 let y = compute_y(x, ll);
209 tof_equation_y(x, y, t0, ll, m)
210}
211
212fn tof_equation_y(x: f64, y: f64, t0: f64, ll: f64, m: u32) -> f64 {
214 let t_ = if m == 0 && x.powi(2) > 0.6 && x.powi(2) < 1.4 {
215 let eta = y - ll * x;
216 let s_1 = (1.0 - ll - x * eta) * 0.5;
217 let q = 4.0 / 3.0 * hyp2f1b(s_1);
218 (eta.powi(3) * q + 4.0 * ll * eta) * 0.5
219 } else {
220 let psi = compute_psi(x, y, ll);
221 let m_float = m as f64;
222 let den = 1.0 - x.powi(2);
223
224 ((psi + m_float * PI) / den.abs().sqrt() - x + ll * y) / den
227 };
228
229 t_ - t0
230}
231
232fn tof_equation_p(x: f64, y: f64, t: f64, ll: f64) -> f64 {
234 (3.0 * t * x - 2.0 + 2.0 * ll.powi(3) * x / y) / (1.0 - x.powi(2))
235}
236
237fn tof_equation_p2(x: f64, y: f64, t: f64, dt: f64, ll: f64) -> f64 {
239 (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))
240}
241
242fn tof_equation_p3(x: f64, y: f64, _t: f64, dt: f64, ddt: f64, ll: f64) -> f64 {
244 (7.0 * x * ddt + 8.0 * dt - 6.0 * (1.0 - ll.powi(2)) * ll.powi(5) * x / y.powi(5))
245 / (1.0 - x.powi(2))
246}
247
248fn compute_t_min(
251 ll: f64,
252 m: u32,
253 maxiter: usize,
254 atol: f64,
255 rtol: f64,
256) -> Result<(f64, f64), LambertError> {
257 if (ll - 1.0).abs() < 1e-9 {
259 let x_t_min = 0.0;
260 let t_min = tof_equation(x_t_min, 0.0, ll, m);
261 Ok((x_t_min, t_min))
262 } else if m == 0 {
263 let x_t_min = f64::INFINITY;
264 let t_min = 0.0;
265 Ok((x_t_min, t_min))
266 } else {
267 let x_i = 0.1;
269 let t_i = tof_equation(x_i, 0.0, ll, m);
270 let x_t_min = halley(x_i, t_i, ll, atol, rtol, maxiter)?;
271 let t_min = tof_equation(x_t_min, 0.0, ll, m);
272 Ok((x_t_min, t_min))
273 }
274}
275
276fn initial_guess(tof_s: f64, ll: f64, m: u32, low_path: bool) -> f64 {
293 if m == 0 {
294 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 {
300 (t_0 / tof_s).powf(2.0 / 3.0) - 1.0
301 } else if tof_s < t_1 {
302 5.0 / 2.0 * t_1 / tof_s * (t_1 - tof_s) / (1.0 - ll.powi(5)) + 1.0
303 } else {
304 (2.0f64.ln() * (tof_s / t_0).ln() / (t_1 / t_0).ln()).exp() - 1.0
307 }
308 } else {
309 let m_float = m as f64;
311
312 let term_l = ((m_float * PI + PI) / (8.0 * tof_s)).powf(2.0 / 3.0);
315 let x_0l = (term_l - 1.0) / (term_l + 1.0);
316
317 let term_r = ((8.0 * tof_s) / (m_float * PI)).powf(2.0 / 3.0);
318 let x_0r = (term_r - 1.0) / (term_r + 1.0);
319
320 if low_path {
322 x_0l.max(x_0r)
323 } else {
324 x_0l.min(x_0r)
325 }
326 }
327}
328
329pub fn hyp2f1b(x: f64) -> f64 {
331 if x >= 1.0 {
332 return f64::INFINITY;
333 }
334
335 let mut res = 1.0;
336 let mut term = 1.0;
337 let mut ii = 0.0_f64; loop {
339 term *= (3.0 + ii) * (1.0 + ii) / (2.5 + ii) * x / (ii + 1.0);
340 let res_old = res;
341 res += term;
342
343 if res == res_old {
345 return res;
346 }
347 ii += 1.0;
348 }
349}
350
351pub fn halley(
356 mut p0: f64,
357 t0: f64,
358 ll: f64,
359 atol: f64,
360 rtol: f64,
361 maxiter: usize,
362) -> Result<f64, LambertError> {
363 for _ in 1..=maxiter {
364 let y = compute_y(p0, ll);
365 let fder = tof_equation_p(p0, y, t0, ll);
366 let fder2 = tof_equation_p2(p0, y, t0, fder, ll);
367
368 if fder2.abs() < 1e-14 {
370 return Err(LambertError::TargetsTooClose);
371 }
372
373 let fder3 = tof_equation_p3(p0, y, t0, fder, fder2, ll);
374
375 let p = p0 - 2.0 * fder * fder2 / (2.0 * fder2.powi(2) - fder * fder3);
377
378 if (p - p0).abs() < rtol * p0.abs() + atol {
379 return Ok(p);
380 }
381 p0 = p;
382 }
383
384 Err(LambertError::SolverMaxIter { maxiter })
385}
386
387pub fn householder(
390 mut p0: f64,
391 t0: f64,
392 ll: f64,
393 m: u32,
394 atol: f64,
395 rtol: f64,
396 maxiter: usize,
397) -> Result<f64, LambertError> {
398 for _ in 1..=maxiter {
399 let y = compute_y(p0, ll);
400 let fval = tof_equation_y(p0, y, t0, ll, m);
401 let t = fval + t0;
402 let fder = tof_equation_p(p0, y, t, ll);
403 let fder2 = tof_equation_p2(p0, y, t, fder, ll);
404 let fder3 = tof_equation_p3(p0, y, t, fder, fder2, ll);
405
406 let num = fder.powi(2) - fval * fder2 / 2.0;
408 let den = fder * (fder.powi(2) - fval * fder2) + fder3 * fval.powi(2) / 6.0;
409
410 if den.abs() < 1e-14 {
411 return Err(LambertError::TargetsTooClose);
412 }
413
414 let p = p0 - fval * (num / den);
415
416 if (p - p0).abs() < rtol * p0.abs() + atol {
417 return Ok(p);
418 }
419 p0 = p;
420 }
421 Err(LambertError::SolverMaxIter { maxiter })
422}
423
424#[cfg(test)]
425mod ut_lambert_izzo {
426 use super::{izzo, LambertInput, TransferKind};
427 use crate::linalg::Vector3;
428 use anise::prelude::{Frame, Orbit};
429 use hifitime::{Epoch, Unit};
430
431 #[test]
432 fn test_lambert_izzo_shortway() {
433 let frame = Frame {
436 ephemeris_id: 301,
437 orientation_id: 0,
438 mu_km3_s2: Some(3.98600433e5),
439 shape: None,
440 };
441 let initial_state = Orbit {
442 radius_km: Vector3::new(15945.34, 0.0, 0.0),
443 velocity_km_s: Vector3::zeros(),
444 epoch: Epoch::from_gregorian_utc_at_midnight(2025, 1, 1),
445 frame,
446 };
447 let final_state = Orbit {
448 radius_km: Vector3::new(12214.83899, 10249.46731, 0.0),
449 velocity_km_s: Vector3::zeros(),
450 epoch: Epoch::from_gregorian_utc_at_midnight(2025, 1, 1) + Unit::Minute * 76.0,
451 frame,
452 };
453
454 let input = LambertInput::from_planetary_states(initial_state, final_state).unwrap();
455
456 let exp_vi = Vector3::new(2.058913, 2.915965, 0.0);
457 let exp_vf = Vector3::new(-3.451565, 0.910315, 0.0);
458
459 let sol = izzo(input, TransferKind::ShortWay).unwrap();
460
461 println!("{sol:?}\t{exp_vi}\t{exp_vf}");
462
463 assert!((sol.v_init_km_s - exp_vi).norm() < 1e-5);
464 assert!((sol.v_final_km_s - exp_vf).norm() < 1e-5);
465 }
466
467 #[test]
468 fn test_lambert_izzo_longway() {
469 let frame = Frame {
471 ephemeris_id: 301,
472 orientation_id: 0,
473 mu_km3_s2: Some(3.98600433e5),
474 shape: None,
475 };
476 let initial_state = Orbit {
477 radius_km: Vector3::new(15945.34, 0.0, 0.0),
478 velocity_km_s: Vector3::zeros(),
479 epoch: Epoch::from_gregorian_utc_at_midnight(2025, 1, 1),
480 frame,
481 };
482 let final_state = Orbit {
483 radius_km: Vector3::new(12214.83899, 10249.46731, 0.0),
484 velocity_km_s: Vector3::zeros(),
485 epoch: Epoch::from_gregorian_utc_at_midnight(2025, 1, 1) + Unit::Minute * 76.0,
486 frame,
487 };
488
489 let input = LambertInput::from_planetary_states(initial_state, final_state).unwrap();
490
491 let exp_vi = Vector3::new(-3.811158, -2.003854, 0.0);
492 let exp_vf = Vector3::new(4.207569, 0.914724, 0.0);
493
494 let sol = izzo(input, TransferKind::LongWay).unwrap();
495
496 assert!((sol.v_init_km_s - exp_vi).norm() < 1e-5);
497 assert!((sol.v_final_km_s - exp_vf).norm() < 1e-5);
498 }
499}