1use std::f64::consts::PI;
4
5use crate::{Direction, LambertError, LambertSolution};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum MultiRevPeriod {
18 LongPeriod,
23 ShortPeriod,
28}
29
30const TWOPI: f64 = 2.0 * PI;
31const SW: f64 = 0.4;
32const TOL: f64 = 1e-12;
33const C0: f64 = 1.7;
34const C1: f64 = 0.5;
35const C2: f64 = 0.03;
36const C3: f64 = 0.15;
37const C41: f64 = 1.0;
38const C42: f64 = 0.24;
39
40fn mag3(v: &[f64; 3]) -> f64 {
42 (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt()
43}
44
45fn dot3(a: &[f64; 3], b: &[f64; 3]) -> f64 {
47 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
48}
49
50fn cross3(a: &[f64; 3], b: &[f64; 3]) -> [f64; 3] {
52 [
53 a[1] * b[2] - a[2] * b[1],
54 a[2] * b[0] - a[0] * b[2],
55 a[0] * b[1] - a[1] * b[0],
56 ]
57}
58
59fn tlamb(
68 m: i32,
69 q: f64,
70 qsqfm1: f64,
71 x: f64,
72 n: i32,
73) -> Result<(f64, f64, f64, f64), LambertError> {
74 let m_f64 = m as f64;
75
76 let qsq = q * q;
77 let xsq = x * x;
78 let u = (1.0 - x) * (1.0 + x); if u.abs() < 1e-10 && (n == -1 || m != 0 || x < 0.0) {
84 return Err(LambertError::ConvergenceFailed);
85 }
86
87 if n != -1 && m == 0 && x >= 0.0 && u.abs() <= SW {
90 let mut u0i = 1.0;
92 let mut u1i = 1.0;
93 let mut u2i = 1.0;
94 let mut u3i = 1.0;
95 let mut term = 4.0;
96 let mut tq = q * qsqfm1;
97 let mut tqsum = if q < 0.5 {
98 1.0 - q * qsq
99 } else {
100 (1.0 / (1.0 + q) + q) * qsqfm1
101 };
102 let mut ttmold = term / 3.0;
103 let mut t = ttmold * tqsum;
104 let mut dt = 0.0;
105 let mut d2t = 0.0;
106 let mut d3t = 0.0;
107
108 let mut i: i32 = 0;
109 loop {
110 i += 1;
111 let p = i as f64;
112 u0i *= u;
113 if n >= 1 && i > 1 {
114 u1i *= u;
115 }
116 if n >= 2 && i > 2 {
117 u2i *= u;
118 }
119 if n >= 3 && i > 3 {
120 u3i *= u;
121 }
122 term *= (p - 0.5) / p;
123 tq *= qsq;
124 tqsum += tq;
125 let told = t;
126 let tterm = term / (2.0 * p + 3.0);
127 let tqterm_base = tterm * tqsum;
128 t -= u0i * ((1.5 * p + 0.25) * tqterm_base / (p * p - 0.25) - ttmold * tq);
129 ttmold = tterm;
130 let tqterm = tqterm_base * p;
131 if n >= 1 {
132 dt += tqterm * u1i;
133 }
134 if n >= 2 {
135 d2t += tqterm * u2i * (p - 1.0);
136 }
137 if n >= 3 {
138 d3t += tqterm * u3i * (p - 1.0) * (p - 2.0);
139 }
140 if i >= n && (t - told).abs() < 1e-15 {
143 break;
144 }
145 if i > 200 {
146 return Err(LambertError::ConvergenceFailed);
147 }
148 }
149
150 if n >= 3 {
151 d3t = 8.0 * x * (1.5 * d2t - xsq * d3t);
152 }
153 if n >= 2 {
154 d2t = 2.0 * (2.0 * xsq * d2t - dt);
155 }
156 if n >= 1 {
157 dt *= -2.0 * x;
158 }
159 t /= xsq;
160
161 return Ok((t, dt, d2t, d3t));
162 }
163
164 let y = u.abs().sqrt();
166 let z = (qsqfm1 + qsq * xsq).sqrt();
167 let qx = q * x;
168
169 let (a, b) = if qx <= 0.0 {
171 (z - qx, q * z - x)
172 } else {
173 let aa = z + qx;
174 let bb = q * z + x;
175 (qsqfm1 / aa, qsqfm1 * (qsq * u - xsq) / bb)
176 };
177
178 if n == -1 {
179 let (aa_v, bb_v) = if qx < 0.0 {
182 (qsqfm1 / a, qsqfm1 * (qsq * u - xsq) / b)
183 } else {
184 (z + qx, q * z + x)
186 };
187 return Ok((0.0, b, bb_v, aa_v));
189 }
190
191 let f = a * y;
193 let t = if x <= 1.0 {
194 let g = if qx * u >= 0.0 {
196 x * z + q * u
197 } else {
198 (xsq - qsq * u) / (x * z - q * u)
199 };
200 m_f64 * PI + f.atan2(g)
201 } else if f > SW {
202 (f + x * z + q * u).ln()
204 } else {
205 let g = x * z + q * u;
207 let fg1 = f / (g + 1.0);
208 let mut term = 2.0 * fg1;
209 let fg1sq = fg1 * fg1;
210 let mut t_acc = term;
211 let mut twoi1 = 1.0;
212 let mut i: i32 = 0;
213 loop {
214 i += 1;
215 twoi1 += 2.0;
216 term *= fg1sq;
217 let told = t_acc;
218 t_acc += term / twoi1;
219 if (t_acc - told).abs() < 1e-15 {
221 break;
222 }
223 if i > 200 {
224 return Err(LambertError::ConvergenceFailed);
225 }
226 }
227 t_acc
228 };
229
230 let t = 2.0 * (t / y + b) / u;
231 let mut dt = 0.0;
232 let mut d2t = 0.0;
233 let mut d3t = 0.0;
234
235 if n >= 1 && z.abs() > 1e-30 {
237 let qz = q / z;
238 let qz2 = qz * qz;
239 let qz3 = qz * qz2;
240 dt = (3.0 * x * t - 4.0 * (a + qx * qsqfm1) / z) / u;
241 if n >= 2 {
242 d2t = (3.0 * t + 5.0 * x * dt + 4.0 * qz3 * qsqfm1) / u;
243 }
244 if n >= 3 {
245 d3t = (8.0 * dt + 7.0 * x * d2t - 12.0 * qz3 * qz2 * x * qsqfm1) / u;
246 }
247 }
248
249 Ok((t, dt, d2t, d3t))
250}
251
252fn xlamb(m: i32, q: f64, qsqfm1: f64, tin: f64, nrev: i32) -> Result<f64, LambertError> {
258 let thr2 = qsqfm1.atan2(2.0 * q) / PI;
259 let m_f64 = m as f64;
260
261 if m == 0 {
262 let (t0, _, _, _) = tlamb(m, q, qsqfm1, 0.0, 0)?;
264 let tdiff = tin - t0;
265
266 let mut x = if tdiff <= 0.0 {
267 t0 * tdiff / (-4.0 * tin)
269 } else {
270 let mut x = -tdiff / (tdiff + 4.0);
271 let w = x + C0 * (2.0 * (1.0 - thr2)).sqrt();
272 if w < 0.0 {
273 x -= (-w).sqrt().sqrt().sqrt().sqrt() * (x + (tdiff / (tdiff + 1.5 * t0)).sqrt());
274 }
275 let w = 4.0 / (4.0 + tdiff);
276 x * (1.0 + x * (C1 * w - C2 * x * w.sqrt()))
277 };
278
279 for _ in 0..3 {
281 let (t, dt, d2t, _) = tlamb(m, q, qsqfm1, x, 2)?;
282 let t_err = tin - t;
283 if dt.abs() > 1e-30 {
285 x += t_err * dt / (dt * dt + t_err * d2t / 2.0);
286 }
287 }
288
289 return Ok(x);
290 }
291
292 let mut xm = 1.0 / (1.5 * (m_f64 + 0.5) * PI);
294 if thr2 < 0.5 {
295 xm *= (2.0 * thr2).sqrt().sqrt().sqrt();
296 } else if thr2 > 0.5 {
297 xm *= 2.0 - (2.0 - 2.0 * thr2).sqrt().sqrt().sqrt();
298 }
299
300 let mut d2t_xm = 0.0;
301 for count in 0..16 {
302 let (_, dt, d2t, d3t) = tlamb(m, q, qsqfm1, xm, 3)?;
303 d2t_xm = d2t;
304 if d2t.abs() < 1e-30 {
306 break;
307 }
308 let xm_old = xm;
309 xm -= dt * d2t / (d2t * d2t - dt * d3t / 2.0);
310 if (xm_old / xm - 1.0).abs() <= TOL {
311 break;
312 }
313 if count == 15 {
314 return Err(LambertError::ConvergenceFailed);
315 }
316 }
317
318 let (tmin, _, _, _) = tlamb(m, q, qsqfm1, xm, 0)?;
319 let tdiffm = tin - tmin;
320
321 if tdiffm < 0.0 {
322 return Err(LambertError::NoSolution);
323 }
324 if tdiffm.abs() < 1e-14 {
326 return Ok(xm);
327 }
328
329 let d2t2 = if d2t_xm.abs() < 1e-30 {
332 3.0 * m_f64 * PI } else {
334 d2t_xm / 2.0
335 };
336
337 let mut x = if nrev > 0 {
338 let mut x = (tdiffm / (d2t2 + tdiffm / ((1.0 - xm).powi(2)))).sqrt();
340 let w = xm + x;
341 let w = w * 4.0 / (4.0 + tdiffm) + (1.0 - w).powi(2);
342 x = x
343 * (1.0
344 - (1.0 + m_f64 + C41 * (thr2 - 0.5)) / (1.0 + C3 * m_f64)
345 * x
346 * (C1 * w + C2 * x * w.sqrt()))
347 + xm;
348 x
349 } else {
350 let (t0, _, _, _) = tlamb(m, q, qsqfm1, 0.0, 0)?;
352 let tdiff0 = t0 - tmin;
353 let tdiff = tin - t0;
354 if tdiff <= 0.0 {
355 xm - (tdiffm / (d2t2 - tdiffm * (d2t2 / tdiff0 - 1.0 / (xm * xm)))).sqrt()
356 } else {
357 let mut x = -tdiff / (tdiff + 4.0);
358 let w = x + C0 * (2.0 * (1.0 - thr2)).sqrt();
359 if w < 0.0 {
360 x -= (-w).sqrt().sqrt().sqrt().sqrt() * (x + (tdiff / (tdiff + 1.5 * t0)).sqrt());
361 }
362 let w = 4.0 / (4.0 + tdiff);
363 x * (1.0
364 + (1.0 + m_f64 + C42 * (thr2 - 0.5)) / (1.0 + C3 * m_f64)
365 * x
366 * (C1 * w - C2 * x * w.sqrt()))
367 }
368 };
369
370 for _ in 0..3 {
372 let (t, dt, d2t, _) = tlamb(m, q, qsqfm1, x, 2)?;
373 let t_err = tin - t;
374 if dt.abs() > 1e-30 {
376 x += t_err * dt / (dt * dt + t_err * d2t / 2.0);
377 }
378 }
379
380 let (t_final, _, _, _) = tlamb(m, q, qsqfm1, x, 0)?;
382 if (tin - t_final).abs() < TOL * tin.max(1.0) {
383 Ok(x)
384 } else {
385 Err(LambertError::ConvergenceFailed)
386 }
387}
388
389pub fn lambert(
413 mu: f64,
414 r1: [f64; 3],
415 r2: [f64; 3],
416 tof: f64,
417 nrev: u32,
418 dir: Direction,
419 period: MultiRevPeriod,
420) -> Result<LambertSolution, LambertError> {
421 if !mu.is_finite() || mu <= 0.0 {
423 return Err(LambertError::InvalidInput("mu must be finite and positive"));
424 }
425 if !tof.is_finite() || tof <= 0.0 {
426 return Err(LambertError::InvalidInput(
427 "tof must be finite and positive",
428 ));
429 }
430 for &v in r1.iter().chain(r2.iter()) {
431 if !v.is_finite() {
432 return Err(LambertError::InvalidInput(
433 "position vector contains non-finite value",
434 ));
435 }
436 }
437 let r1_mag = mag3(&r1);
438 let r2_mag = mag3(&r2);
439 if r1_mag < 1e-10 * r2_mag.max(1.0) {
441 return Err(LambertError::InvalidInput(
442 "r1 has zero or near-zero magnitude",
443 ));
444 }
445 if r2_mag < 1e-10 * r1_mag.max(1.0) {
447 return Err(LambertError::InvalidInput(
448 "r2 has zero or near-zero magnitude",
449 ));
450 }
451
452 let cos_th = dot3(&r1, &r2) / (r1_mag * r2_mag);
456 if cos_th <= -1.0 + 1e-10 {
457 return Err(LambertError::SingularTransfer);
458 }
459 let cross_z = cross3(&r1, &r2);
460 let cross_z_mag = mag3(&cross_z);
461 if cross_z_mag < 1e-10 * r1_mag * r2_mag {
463 return Err(LambertError::SingularTransfer);
464 }
465
466 let nrev_abs: i32 = i32::try_from(nrev).map_err(|_| {
472 LambertError::InvalidInput("nrev exceeds maximum supported revolution count")
473 })?;
474 let nrev_signed = match period {
475 MultiRevPeriod::LongPeriod => nrev_abs,
476 MultiRevPeriod::ShortPeriod => -nrev_abs,
477 };
478
479 let signed_tdelt = match dir {
482 Direction::Prograde => tof,
483 Direction::Retrograde => -tof,
484 };
485
486 let mut v1 = [0.0_f64; 3];
487 let mut v2 = [0.0_f64; 3];
488 let code = gooding_lambert(mu, &r1, &r2, nrev_signed, signed_tdelt, &mut v1, &mut v2);
489 match code {
490 c if c > 0 => Ok(LambertSolution { v1, v2 }),
491 0 => Err(LambertError::NoSolution),
492 _ => Err(LambertError::ConvergenceFailed),
493 }
494}
495
496#[allow(clippy::too_many_arguments)]
504fn vlamb_c(
505 gm: f64,
506 r1: f64,
507 r2: f64,
508 th: f64,
509 nrev: i32,
510 tdelt: f64,
511 v1: &mut [f64; 2],
512 v2: &mut [f64; 2],
513) -> i32 {
514 let m = nrev.unsigned_abs() as i32;
515 let thr2 = th / 2.0;
516 let dr = r1 - r2;
517 let r1r2th = 4.0 * r1 * r2 * thr2.sin().powi(2);
518 let csq = dr * dr + r1r2th;
519 let c = csq.sqrt();
520 let s = (r1 + r2 + c) / 2.0;
521 let gms = (gm * s / 2.0).sqrt();
522 let qsqfm1 = c / s;
523 let q = (r1 * r2).sqrt() * thr2.cos() / s;
524
525 let (rho, sig) = if c > 1e-14 {
527 (dr / c, r1r2th / csq)
528 } else {
529 (0.0, 1.0)
530 };
531
532 let t = 4.0 * gms * tdelt / (s * s);
533
534 let x = match xlamb(m, q, qsqfm1, t, nrev) {
535 Ok(x) => x,
536 Err(LambertError::NoSolution) => return 0,
537 Err(_) => return -1,
538 };
539
540 let (_, qzminx, qzplx, zplqx) = match tlamb(m, q, qsqfm1, x, -1) {
541 Ok(v) => v,
542 Err(_) => return -1,
543 };
544
545 v1[1] = gms * zplqx * sig.sqrt();
547 v2[1] = v1[1] / r2;
548 v1[1] /= r1;
549 v1[0] = gms * (qzminx - qzplx * rho) / r1;
550 v2[0] = -gms * (qzminx + qzplx * rho) / r2;
551
552 1
553}
554
555pub fn gooding_lambert(
584 gm: f64,
585 r1: &[f64; 3],
586 r2: &[f64; 3],
587 nrev: i32,
588 tdelt: f64,
589 v1: &mut [f64; 3],
590 v2: &mut [f64; 3],
591) -> i32 {
592 let rad1 = mag3(r1);
593 let rad2 = mag3(r2);
594
595 let dot = dot3(r1, r2);
596 let th = (dot / (rad1 * rad2)).clamp(-1.0, 1.0).acos();
597
598 let mut va1 = [0.0_f64; 2];
599 let mut va2 = [0.0_f64; 2];
600
601 let retrograde = tdelt < 0.0;
606 let (th, tdelt) = if retrograde {
607 (TWOPI - th, tdelt.abs())
608 } else {
609 (th, tdelt)
610 };
611
612 let code = vlamb_c(gm, rad1, rad2, th, nrev, tdelt, &mut va1, &mut va2);
613
614 if code > 0 {
615 let x1 = [r1[0] / rad1, r1[1] / rad1, r1[2] / rad1];
616 let x2 = [r2[0] / rad2, r2[1] / rad2, r2[2] / rad2];
617
618 let mut z = cross3(&x1, &x2);
619 let zm = mag3(&z);
620
621 if zm < 1e-10 {
623 z = [0.0, 0.0, 1.0];
624 } else {
625 z[0] /= zm;
626 z[1] /= zm;
627 z[2] /= zm;
628 }
629
630 if retrograde {
632 z[0] = -z[0];
633 z[1] = -z[1];
634 z[2] = -z[2];
635 }
636
637 let y1 = cross3(&z, &x1);
638 let y2 = cross3(&z, &x2);
639
640 v1[0] = va1[0] * x1[0] + va1[1] * y1[0];
641 v1[1] = va1[0] * x1[1] + va1[1] * y1[1];
642 v1[2] = va1[0] * x1[2] + va1[1] * y1[2];
643 v2[0] = va2[0] * x2[0] + va2[1] * y2[0];
644 v2[1] = va2[0] * x2[1] + va2[1] * y2[1];
645 v2[2] = va2[0] * x2[2] + va2[1] * y2[2];
646 }
647
648 code
649}
650
651#[cfg(test)]
652mod tests {
653 use super::*;
654 use std::f64::consts::PI;
655
656 fn mag(v: [f64; 3]) -> f64 {
657 mag3(&v)
658 }
659
660 fn cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
661 cross3(&a, &b)
662 }
663
664 fn energy(r: [f64; 3], v: [f64; 3], mu: f64) -> f64 {
666 let v_sq = v[0] * v[0] + v[1] * v[1] + v[2] * v[2];
667 v_sq / 2.0 - mu / mag(r)
668 }
669
670 #[test]
671 fn tlamb_at_zero() {
672 let (t, dt, d2t, _) = tlamb(0, 0.5, 0.5, 0.0, 2).unwrap();
674 assert!(t.is_finite() && t > 0.0, "T(0) should be positive, got {t}");
675 assert!(dt.is_finite(), "dT/dx at 0 should be finite, got {dt}");
676 assert!(d2t.is_finite(), "d²T/dx² at 0 should be finite, got {d2t}");
677 }
678
679 #[test]
680 fn tlamb_velocity_mode() {
681 let (t0, qzminx, qzplx, zplqx) = tlamb(0, 0.5, 0.5, 0.3, -1).unwrap();
683 assert_eq!(t0, 0.0);
684 assert!(qzminx.is_finite());
685 assert!(qzplx.is_finite());
686 assert!(zplqx.is_finite());
687 }
688
689 #[test]
690 fn energy_conserved_prograde() {
691 let mu = 1.0;
693 let r1 = [1.0, 0.0, 0.0];
694 let r2 = [0.0, 1.0, 0.0];
695 let sol = lambert(mu, r1, r2, PI / 2.0, 0, Direction::Prograde, MultiRevPeriod::LongPeriod).unwrap();
696 let e1 = energy(r1, sol.v1, mu);
697 let e2 = energy(r2, sol.v2, mu);
698 assert!((e1 - e2).abs() < 1e-12, "energy: {e1} vs {e2}");
700 }
701
702 #[test]
703 fn angular_momentum_conserved() {
704 let mu = 1.0;
706 let r1 = [1.0, 0.0, 0.0];
707 let r2 = [0.0, 1.5, 0.0];
708 let sol = lambert(mu, r1, r2, 2.0, 0, Direction::Prograde, MultiRevPeriod::LongPeriod).unwrap();
709 let h1 = cross(r1, sol.v1);
710 let h2 = cross(r2, sol.v2);
711 for i in 0..3 {
712 assert!(
714 (h1[i] - h2[i]).abs() < 1e-12,
715 "h[{i}]: {:.6e} vs {:.6e}",
716 h1[i],
717 h2[i]
718 );
719 }
720 }
721
722 #[test]
723 fn prograde_positive_hz() {
724 let mu = 1.0;
726 let r1 = [1.0, 0.0, 0.0];
727 let r2 = [0.0, 1.0, 0.0];
728 let sol = lambert(mu, r1, r2, PI / 2.0, 0, Direction::Prograde, MultiRevPeriod::LongPeriod).unwrap();
729 let h = cross(r1, sol.v1);
730 assert!(
731 h[2] > 0.0,
732 "hz should be positive for prograde, got {}",
733 h[2]
734 );
735 }
736
737 #[test]
738 fn retrograde_negative_hz() {
739 let mu = 1.0;
742 let r1 = [1.0, 0.0, 0.0];
743 let r2 = [0.0, 1.0, 0.0];
744 let sol = lambert(mu, r1, r2, PI / 2.0, 0, Direction::Retrograde, MultiRevPeriod::LongPeriod).unwrap();
745 let h = cross(r1, sol.v1);
746 assert!(
747 h[2] < 0.0,
748 "hz should be negative for retrograde, got {}",
749 h[2]
750 );
751 }
752
753 #[test]
754 fn leo_to_geo() {
755 let mu = 398600.4418;
757 let r1 = [6678.0, 0.0, 0.0];
758 let r2 = [0.0, 42164.0, 0.0];
759 let sol = lambert(mu, r1, r2, 5.0 * 3600.0, 0, Direction::Prograde, MultiRevPeriod::LongPeriod).unwrap();
760 let speed = mag(sol.v1);
761 assert!(
762 (7.0..=12.0).contains(&speed),
763 "LEO departure speed {speed:.2} km/s out of range"
764 );
765 }
766
767 #[test]
768 fn singular_transfer_180_deg() {
769 let mu = 1.0;
771 let r1 = [1.0, 0.0, 0.0];
772 let r2 = [-1.0, 0.0, 0.0];
773 assert_eq!(
774 lambert(mu, r1, r2, 1.0, 0, Direction::Prograde, MultiRevPeriod::LongPeriod),
775 Err(LambertError::SingularTransfer)
776 );
777 }
778
779 #[test]
780 fn invalid_mu() {
781 let r1 = [1.0, 0.0, 0.0];
782 let r2 = [0.0, 1.0, 0.0];
783 assert!(matches!(
784 lambert(0.0, r1, r2, 1.0, 0, Direction::Prograde, MultiRevPeriod::LongPeriod),
785 Err(LambertError::InvalidInput(_))
786 ));
787 assert!(matches!(
788 lambert(-1.0, r1, r2, 1.0, 0, Direction::Prograde, MultiRevPeriod::LongPeriod),
789 Err(LambertError::InvalidInput(_))
790 ));
791 assert!(matches!(
792 lambert(f64::NAN, r1, r2, 1.0, 0, Direction::Prograde, MultiRevPeriod::LongPeriod),
793 Err(LambertError::InvalidInput(_))
794 ));
795 }
796
797 #[test]
798 fn invalid_tof() {
799 let mu = 1.0;
800 let r1 = [1.0, 0.0, 0.0];
801 let r2 = [0.0, 1.0, 0.0];
802 assert!(matches!(
803 lambert(mu, r1, r2, 0.0, 0, Direction::Prograde, MultiRevPeriod::LongPeriod),
804 Err(LambertError::InvalidInput(_))
805 ));
806 assert!(matches!(
807 lambert(mu, r1, r2, -1.0, 0, Direction::Prograde, MultiRevPeriod::LongPeriod),
808 Err(LambertError::InvalidInput(_))
809 ));
810 }
811
812 #[test]
813 fn hyperbolic_positive_energy() {
814 let mu = 1.0;
816 let r1 = [1.0, 0.0, 0.0];
817 let r2 = [0.0, 1.0, 0.0];
818 let sol = lambert(mu, r1, r2, 0.1, 0, Direction::Prograde, MultiRevPeriod::LongPeriod).unwrap();
819 let e = energy(r1, sol.v1, mu);
820 assert!(e > 0.0, "Short TOF should give hyperbolic (e>0), got e={e}");
821 }
822
823 #[test]
824 fn three_dimensional_transfer() {
825 let mu = 1.0;
827 let r1 = [1.0, 0.0, 0.0];
828 let r2 = [0.0, 0.8, 0.6];
829 let sol = lambert(mu, r1, r2, PI / 2.0, 0, Direction::Prograde, MultiRevPeriod::LongPeriod).unwrap();
830 assert!(
831 sol.v1[2].abs() > 1e-6,
832 "Expected 3D velocity, got v1z={}",
833 sol.v1[2]
834 );
835 let e1 = energy(r1, sol.v1, mu);
837 let e2 = energy(r2, sol.v2, mu);
838 assert!((e1 - e2).abs() < 1e-12, "3D energy: {e1} vs {e2}");
839 }
840
841 #[test]
842 fn multi_rev_longer_tof() {
843 let mu = 1.0;
845 let r1 = [1.0, 0.0, 0.0];
846 let r2 = [-1.0, 0.1, 0.0];
847 let sol0 = lambert(mu, r1, r2, 20.0, 0, Direction::Prograde, MultiRevPeriod::LongPeriod).unwrap();
849 let sol1 = lambert(mu, r1, r2, 20.0, 1, Direction::Prograde, MultiRevPeriod::LongPeriod).unwrap();
851 let e0 = energy(r1, sol0.v1, mu);
853 let e1 = energy(r1, sol1.v1, mu);
854 assert!(
855 (e0 - e1).abs() > 1e-6,
856 "0-rev and 1-rev should have different energies"
857 );
858 }
859
860 #[test]
861 fn lambert_wraps_gooding_lambert() {
862 let mu = 398600.4418;
865 let r1 = [6678.0, 0.0, 0.0];
866 let r2 = [0.0, 42164.0, 0.0];
867 let tof = 5.0 * 3600.0;
868
869 let sol = lambert(mu, r1, r2, tof, 0, Direction::Prograde, MultiRevPeriod::LongPeriod).unwrap();
871
872 let mut v1 = [0.0_f64; 3];
874 let mut v2 = [0.0_f64; 3];
875 let code = gooding_lambert(mu, &r1, &r2, 0, tof, &mut v1, &mut v2);
876 assert!(code > 0);
877
878 assert_eq!(
880 sol.v1, v1,
881 "v1 mismatch between lambert() and gooding_lambert()"
882 );
883 assert_eq!(
884 sol.v2, v2,
885 "v2 mismatch between lambert() and gooding_lambert()"
886 );
887 }
888
889 #[test]
890 fn lambert_retrograde_physics() {
891 let mu = 1.0;
894 let r1 = [1.0, 0.0, 0.0];
895 let r2 = [0.0, 1.0, 0.0];
896 let tof = PI / 2.0;
897
898 let sol = lambert(mu, r1, r2, tof, 0, Direction::Retrograde, MultiRevPeriod::LongPeriod).unwrap();
899
900 let e1 = energy(r1, sol.v1, mu);
902 let e2 = energy(r2, sol.v2, mu);
903 assert!((e1 - e2).abs() < 1e-12, "energy: {e1} vs {e2}");
904
905 let h1 = cross(r1, sol.v1);
907 let h2 = cross(r2, sol.v2);
908 for i in 0..3 {
909 assert!(
910 (h1[i] - h2[i]).abs() < 1e-12,
911 "h[{i}]: {:.6e} vs {:.6e}",
912 h1[i],
913 h2[i]
914 );
915 }
916
917 let h_ref = cross(r1, r2);
919 let dot_h = h_ref[0] * h1[0] + h_ref[1] * h1[1] + h_ref[2] * h1[2];
920 assert!(
921 dot_h < 0.0,
922 "dot(r1×r2, r1×v1) should be negative for retrograde, got {}",
923 dot_h
924 );
925 }
926
927 #[test]
928 fn lambert_wraps_gooding_lambert_multirev() {
929 let mu = 1.0;
930 let r1 = [1.0, 0.0, 0.0];
931 let r2 = [-1.0, 0.1, 0.0];
932 let tof = 20.0;
933
934 let sol = lambert(mu, r1, r2, tof, 1, Direction::Prograde, MultiRevPeriod::LongPeriod).unwrap();
935
936 let mut v1 = [0.0_f64; 3];
937 let mut v2 = [0.0_f64; 3];
938 let code = gooding_lambert(mu, &r1, &r2, 1, tof, &mut v1, &mut v2);
939 assert!(code > 0);
940
941 assert_eq!(sol.v1, v1);
942 assert_eq!(sol.v2, v2);
943 }
944
945 #[test]
950 fn tlamb_sw_boundary_series_path() {
951 let q = 0.5;
955 let qsqfm1 = 1.0 - q * q; let x = 0.8;
957
958 let (t, dt, d2t, d3t) = tlamb(0, q, qsqfm1, x, 3).unwrap();
959 assert!(t.is_finite() && t > 0.0, "T should be positive on series path, got {t}");
960 assert!(dt.is_finite(), "dT should be finite on series path, got {dt}");
961 assert!(d2t.is_finite(), "d2T should be finite on series path, got {d2t}");
962 assert!(d3t.is_finite(), "d3T should be finite on series path, got {d3t}");
963 }
964
965 #[test]
966 fn tlamb_sw_boundary_series_vs_direct_continuity() {
967 let q = 0.5;
973 let qsqfm1 = 1.0 - q * q;
974
975 let (t_direct, _, _, _) = tlamb(0, q, qsqfm1, 0.77, 0).unwrap();
976 let (t_series, _, _, _) = tlamb(0, q, qsqfm1, 0.78, 0).unwrap();
977
978 let rel_diff = (t_direct - t_series).abs() / t_direct.max(t_series);
981 assert!(
982 rel_diff < 0.1,
983 "Series and direct paths should be continuous at SW boundary: \
984 T(0.77)={t_direct}, T(0.78)={t_series}, rel_diff={rel_diff}"
985 );
986 }
987
988 #[test]
989 fn tlamb_sw_boundary_large_q() {
990 let q = 0.8;
993 let qsqfm1 = 1.0 - q * q; let x = 0.85; let (t, _, _, _) = tlamb(0, q, qsqfm1, x, 0).unwrap();
997 assert!(t.is_finite() && t > 0.0, "T should be positive for large-q series, got {t}");
998 }
999
1000 #[test]
1001 fn tlamb_hyperbolic_large_f() {
1002 let q = 0.5;
1005 let qsqfm1 = 1.0 - q * q;
1006 let x = 2.0; let (t, _, _, _) = tlamb(0, q, qsqfm1, x, 0).unwrap();
1009 assert!(t.is_finite() && t > 0.0, "T should be positive on hyperbolic branch, got {t}");
1010 }
1011
1012 #[test]
1013 fn tlamb_hyperbolic_with_derivatives() {
1014 let q = 0.5;
1016 let qsqfm1 = 1.0 - q * q;
1017 let x = 1.5; let (t, dt, d2t, d3t) = tlamb(0, q, qsqfm1, x, 3).unwrap();
1020 assert!(t.is_finite(), "T should be finite, got {t}");
1021 assert!(dt.is_finite(), "dT should be finite, got {dt}");
1022 assert!(d2t.is_finite(), "d2T should be finite, got {d2t}");
1023 assert!(d3t.is_finite(), "d3T should be finite, got {d3t}");
1024 assert!(dt < 0.0, "dT/dx should be negative for hyperbolic x, got {dt}");
1026 }
1027
1028 #[test]
1029 fn tlamb_hyperbolic_small_f_log_series() {
1030 let q = 0.5;
1034 let qsqfm1 = 1.0 - q * q;
1035 let x = 1.01; let (t, _, _, _) = tlamb(0, q, qsqfm1, x, 0).unwrap();
1038 assert!(t.is_finite() && t > 0.0, "T should be positive on hyp small-f path, got {t}");
1039 }
1040
1041 #[test]
1042 fn tlamb_multi_rev() {
1043 let q = 0.5;
1046 let qsqfm1 = 1.0 - q * q;
1047 let x = 0.3;
1048
1049 let (t0, _, _, _) = tlamb(0, q, qsqfm1, x, 0).unwrap();
1050 let (t1, _, _, _) = tlamb(1, q, qsqfm1, x, 0).unwrap();
1051 let (t2, _, _, _) = tlamb(2, q, qsqfm1, x, 0).unwrap();
1052
1053 assert!(t0.is_finite() && t0 > 0.0, "T(m=0) should be positive, got {t0}");
1054 assert!(t1.is_finite() && t1 > 0.0, "T(m=1) should be positive, got {t1}");
1055 assert!(t2.is_finite() && t2 > 0.0, "T(m=2) should be positive, got {t2}");
1056 assert!(t1 > t0, "T(m=1)={t1} should exceed T(m=0)={t0}");
1058 assert!(t2 > t1, "T(m=2)={t2} should exceed T(m=1)={t1}");
1059 }
1060
1061 #[test]
1062 fn tlamb_multi_rev_with_derivatives() {
1063 let q = 0.5;
1065 let qsqfm1 = 1.0 - q * q;
1066 let x = 0.3;
1067
1068 let (t, dt, d2t, d3t) = tlamb(1, q, qsqfm1, x, 3).unwrap();
1069 assert!(t.is_finite(), "T should be finite for m=1, got {t}");
1070 assert!(dt.is_finite(), "dT should be finite for m=1, got {dt}");
1071 assert!(d2t.is_finite(), "d2T should be finite for m=1, got {d2t}");
1072 assert!(d3t.is_finite(), "d3T should be finite for m=1, got {d3t}");
1073 }
1074
1075 #[test]
1076 fn tlamb_u_near_zero_guard_velocity_mode() {
1077 let q = 0.5;
1080 let qsqfm1 = 1.0 - q * q;
1081 let x = 1.0 - 1e-12; let result = tlamb(0, q, qsqfm1, x, -1);
1084 assert!(
1085 matches!(result, Err(LambertError::ConvergenceFailed)),
1086 "u-near-zero guard should fire for n=-1, got {result:?}"
1087 );
1088 }
1089
1090 #[test]
1091 fn tlamb_u_near_zero_guard_multi_rev() {
1092 let q = 0.5;
1094 let qsqfm1 = 1.0 - q * q;
1095 let x = 1.0 - 1e-12;
1096
1097 let result = tlamb(1, q, qsqfm1, x, 0);
1098 assert!(
1099 matches!(result, Err(LambertError::ConvergenceFailed)),
1100 "u-near-zero guard should fire for m=1, got {result:?}"
1101 );
1102 }
1103
1104 #[test]
1105 fn tlamb_u_near_zero_guard_negative_x() {
1106 let q = 0.5;
1108 let qsqfm1 = 1.0 - q * q;
1109 let x = -(1.0 - 1e-12);
1110
1111 let result = tlamb(0, q, qsqfm1, x, 0);
1112 assert!(
1113 matches!(result, Err(LambertError::ConvergenceFailed)),
1114 "u-near-zero guard should fire for negative x near -1, got {result:?}"
1115 );
1116 }
1117
1118 #[test]
1119 fn tlamb_u_near_zero_safe_series_path() {
1120 let q = 0.5;
1123 let qsqfm1 = 1.0 - q * q;
1124 let x = 1.0 - 1e-12; let result = tlamb(0, q, qsqfm1, x, 0);
1132 assert!(
1133 result.is_ok(),
1134 "Series path should handle u→0 safely for m=0, x>=0, n=0, got {result:?}"
1135 );
1136 }
1137
1138 #[test]
1143 fn xlamb_single_rev_converges() {
1144 let q = 0.5;
1147 let qsqfm1 = 1.0 - q * q;
1148 let x_ref = 0.3;
1149
1150 let (t_target, _, _, _) = tlamb(0, q, qsqfm1, x_ref, 0).unwrap();
1151 let x_found = xlamb(0, q, qsqfm1, t_target, 0).unwrap();
1152
1153 assert!(
1154 (x_found - x_ref).abs() < 1e-8,
1155 "xlamb should recover x={x_ref}, got {x_found}"
1156 );
1157 }
1158
1159 #[test]
1160 fn xlamb_single_rev_elliptic_side() {
1161 let q = 0.5;
1164 let qsqfm1 = 1.0 - q * q;
1165
1166 let (t0, _, _, _) = tlamb(0, q, qsqfm1, 0.0, 0).unwrap();
1168 let tin = t0 * 0.8; let x = xlamb(0, q, qsqfm1, tin, 0).unwrap();
1172
1173 assert!(x.is_finite(), "Should converge on elliptic side, got {x}");
1174 let (t_check, _, _, _) = tlamb(0, q, qsqfm1, x, 0).unwrap();
1176 assert!(
1177 (t_check - tin).abs() < 1e-10 * tin,
1178 "T(x_found) should match target: T={t_check}, target={tin}"
1179 );
1180 }
1181
1182 #[test]
1183 fn xlamb_single_rev_hyperbolic_side() {
1184 let q = 0.5;
1187 let qsqfm1 = 1.0 - q * q;
1188
1189 let (t0, _, _, _) = tlamb(0, q, qsqfm1, 0.0, 0).unwrap();
1190 let tin = t0 * 1.5;
1192 let x = xlamb(0, q, qsqfm1, tin, 0).unwrap();
1193
1194 assert!(x.is_finite(), "Should converge on hyperbolic side, got {x}");
1195 let (t_check, _, _, _) = tlamb(0, q, qsqfm1, x, 0).unwrap();
1196 assert!(
1197 (t_check - tin).abs() < 1e-10 * tin,
1198 "T(x_found) should match target: T={t_check}, target={tin}"
1199 );
1200 }
1201
1202 #[test]
1203 fn xlamb_multi_rev_long_period() {
1204 let q = 0.5;
1206 let qsqfm1 = 1.0 - q * q;
1207 let x_ref = 0.3;
1208
1209 let (t_target, _, _, _) = tlamb(1, q, qsqfm1, x_ref, 0).unwrap();
1211 let x_found = xlamb(1, q, qsqfm1, t_target, 1).unwrap(); assert!(
1214 (x_found - x_ref).abs() < 1e-6,
1215 "xlamb long-period should recover x={x_ref}, got {x_found}"
1216 );
1217 }
1218
1219 #[test]
1220 fn xlamb_multi_rev_short_period() {
1221 let q = 0.5;
1223 let qsqfm1 = 1.0 - q * q;
1224
1225 let x_ref = -0.3; let (t_target, _, _, _) = tlamb(1, q, qsqfm1, x_ref, 0).unwrap();
1231 let x_found = xlamb(1, q, qsqfm1, t_target, -1).unwrap(); assert!(
1234 (x_found - x_ref).abs() < 1e-6,
1235 "xlamb short-period should recover x={x_ref}, got {x_found}"
1236 );
1237 }
1238
1239 #[test]
1240 fn xlamb_multi_rev_bifurcation_two_solutions() {
1241 let q = 0.5;
1244 let qsqfm1 = 1.0 - q * q;
1245
1246 let x_ref_long = 0.3;
1250 let (t_target, _, _, _) = tlamb(1, q, qsqfm1, x_ref_long, 0).unwrap();
1251
1252 let x_long = xlamb(1, q, qsqfm1, t_target, 1).unwrap();
1253 let x_short = xlamb(1, q, qsqfm1, t_target, -1).unwrap();
1254
1255 let (t_long, _, _, _) = tlamb(1, q, qsqfm1, x_long, 0).unwrap();
1257 let (t_short, _, _, _) = tlamb(1, q, qsqfm1, x_short, 0).unwrap();
1258 assert!(
1259 (t_long - t_target).abs() < 1e-8 * t_target,
1260 "Long-period T should match: {t_long} vs {t_target}"
1261 );
1262 assert!(
1263 (t_short - t_target).abs() < 1e-8 * t_target,
1264 "Short-period T should match: {t_short} vs {t_target}"
1265 );
1266
1267 assert!(
1269 (x_long - x_short).abs() > 1e-6,
1270 "Long and short period should give different x: long={x_long}, short={x_short}"
1271 );
1272 }
1273
1274 #[test]
1275 fn xlamb_multi_rev_no_solution_below_tmin() {
1276 let q = 0.5;
1278 let qsqfm1 = 1.0 - q * q;
1279
1280 let result = xlamb(1, q, qsqfm1, 0.1, 1);
1282 assert!(
1283 matches!(result, Err(LambertError::NoSolution)),
1284 "Below T_min should return NoSolution, got {result:?}"
1285 );
1286 }
1287
1288 #[test]
1289 fn xlamb_multi_rev_at_tmin_returns_xm() {
1290 let q = 0.5;
1292 let qsqfm1 = 1.0 - q * q;
1293
1294 let mut xm = 1.0 / (1.5 * 1.5 * PI); for _ in 0..20 {
1298 let (_, dt, d2t, d3t) = tlamb(1, q, qsqfm1, xm, 3).unwrap();
1299 if d2t.abs() < 1e-30 {
1300 break;
1301 }
1302 xm -= dt * d2t / (d2t * d2t - dt * d3t / 2.0);
1303 }
1304 let (tmin, _, _, _) = tlamb(1, q, qsqfm1, xm, 0).unwrap();
1305
1306 let x_found = xlamb(1, q, qsqfm1, tmin, 1).unwrap();
1308 assert!(
1309 (x_found - xm).abs() < 1e-6,
1310 "At T_min, xlamb should return xm={xm}, got {x_found}"
1311 );
1312 }
1313
1314 #[test]
1315 fn xlamb_single_rev_roundtrip_via_lambert() {
1316 let mu = 1.0;
1320 let r1 = [1.0, 0.0, 0.0];
1321 let r2 = [0.0, 2.0, 0.0];
1322 let tof = 3.0;
1323
1324 let sol = lambert(mu, r1, r2, tof, 0, Direction::Prograde, MultiRevPeriod::LongPeriod).unwrap();
1325 let e1 = energy(r1, sol.v1, mu);
1326 let e2 = energy(r2, sol.v2, mu);
1327 assert!(
1328 (e1 - e2).abs() < 1e-11,
1329 "Energy conservation verifies xlamb convergence: {e1} vs {e2}"
1330 );
1331 }
1332
1333 #[test]
1334 fn xlamb_multi_rev_short_period_tdiff_positive() {
1335 let q = 0.5;
1338 let qsqfm1 = 1.0 - q * q;
1339
1340 let (t0, _, _, _) = tlamb(1, q, qsqfm1, 0.0, 0).unwrap();
1342 let tin = t0 * 1.2;
1345 let result = xlamb(1, q, qsqfm1, tin, -1);
1346
1347 assert!(
1349 result.is_ok() || matches!(result, Err(LambertError::NoSolution | LambertError::ConvergenceFailed)),
1350 "Short-period tdiff>0 branch should be handled, got {result:?}"
1351 );
1352 if let Ok(x) = result {
1354 let (t_check, _, _, _) = tlamb(1, q, qsqfm1, x, 0).unwrap();
1355 assert!(
1356 (t_check - tin).abs() < 1e-8 * tin,
1357 "Root should satisfy T(x)=tin: T={t_check}, tin={tin}"
1358 );
1359 }
1360 }
1361}