nyx_space/tools/lambert/
mod.rs1use crate::cosmic::AstroError;
20use crate::errors::LambertError;
21use crate::linalg::Vector3;
22use std::f64::consts::PI;
23
24mod godding;
25mod izzo;
26
27use anise::errors::PhysicsError;
28use anise::prelude::Orbit;
29pub use godding::gooding;
30pub use izzo::izzo;
31
32const TAU: f64 = 2.0 * PI;
33const LAMBERT_EPSILON: f64 = 1e-4; const LAMBERT_EPSILON_TIME: f64 = 1e-4; const LAMBERT_EPSILON_RAD: f64 = (5e-5 / 180.0) * PI; const MAX_ITERATIONS: usize = 1000;
39
40pub enum TransferKind {
42 Auto,
43 ShortWay,
44 LongWay,
45 NRevs(u8),
46}
47
48impl TransferKind {
49 fn direction_of_motion(
60 self,
61 r_final: &Vector3<f64>,
62 r_init: &Vector3<f64>,
63 ) -> Result<f64, LambertError> {
64 match self {
65 TransferKind::Auto => {
66 let mut dnu = r_final[1].atan2(r_final[0]) - r_init[1].atan2(r_init[0]);
67 if dnu < 0.0 {
68 dnu += TAU;
69 }
70
71 if dnu > std::f64::consts::PI {
72 Ok(-1.0)
73 } else {
74 Ok(1.0)
75 }
76 }
77 TransferKind::ShortWay => Ok(1.0),
78 TransferKind::LongWay => Ok(-1.0),
79 _ => Err(LambertError::MultiRevNotSupported),
80 }
81 }
82}
83
84#[derive(Copy, Clone, Debug)]
85pub struct LambertInput {
86 pub initial_state: Orbit,
87 pub final_state: Orbit,
88}
89
90impl LambertInput {
91 pub fn from_planetary_states(
92 initial_state: Orbit,
93 final_state: Orbit,
94 ) -> Result<Self, AstroError> {
95 if final_state.frame != initial_state.frame {
96 return Err(AstroError::AstroPhysics {
97 source: PhysicsError::FrameMismatch {
98 action: "Lambert solver requires both states to be in the same frame",
99 frame1: final_state.frame.into(),
100 frame2: initial_state.frame.into(),
101 },
102 });
103 }
104 initial_state
106 .frame
107 .mu_km3_s2()
108 .map_err(|e| AstroError::AstroPhysics { source: e })?;
109
110 Ok(Self {
111 initial_state,
112 final_state,
113 })
114 }
115
116 pub fn mu_km2_s3(&self) -> f64 {
118 self.initial_state.frame.mu_km3_s2().unwrap()
119 }
120}
121
122#[derive(Copy, Clone, Debug)]
124pub struct LambertSolution {
125 pub v_init_km_s: Vector3<f64>,
126 pub v_final_km_s: Vector3<f64>,
127 pub phi_rad: f64,
129 pub input: LambertInput,
130}
131
132impl LambertSolution {
133 pub fn v_inf_outgoing_km_s(&self) -> Vector3<f64> {
135 self.input.initial_state.velocity_km_s - self.v_init_km_s
136 }
137
138 pub fn v_inf_incoming_km_s(&self) -> Vector3<f64> {
140 self.input.final_state.velocity_km_s - self.v_final_km_s
141 }
142
143 pub fn transfer_orbit(mut self) -> Orbit {
145 self.input.initial_state.velocity_km_s = self.v_init_km_s;
146 self.input.initial_state
147 }
148
149 pub fn arrival_orbit(mut self) -> Orbit {
151 self.input.final_state.velocity_km_s = self.v_final_km_s;
152 self.input.final_state
153 }
154
155 pub fn v_inf_outgoing_declination_deg(&self) -> f64 {
157 let v_inf_km_s = -self.v_inf_outgoing_km_s();
159 (v_inf_km_s.z / v_inf_km_s.norm()).asin().to_degrees()
160 }
161
162 pub fn v_inf_outgoing_right_ascension_deg(&self) -> f64 {
164 let v_inf_km_s = -self.v_inf_outgoing_km_s();
166 (v_inf_km_s.y.atan2(v_inf_km_s.x)).to_degrees()
167 }
168
169 pub fn c3_km2_s2(&self) -> f64 {
171 self.v_inf_outgoing_km_s().norm_squared()
172 }
173}
174
175#[cfg(test)]
176mod ut_lambert_transfer_kind {
177 use super::{TransferKind, Vector3};
178
179 #[test]
180 fn test_auto_direction_of_motion_long_way() {
181 let r_init = Vector3::new(8000.0, 0.0, 0.0);
185 let r_final = Vector3::new(0.0, -8000.0, 0.0);
186
187 let dm = TransferKind::Auto
188 .direction_of_motion(&r_final, &r_init)
189 .unwrap();
190
191 assert_eq!(dm, -1.0, "Auto should pick long-way for Δν=3π/2; got {dm}");
192 }
193
194 #[test]
195 fn test_auto_direction_of_motion_short_way() {
196 let r_init = Vector3::new(8000.0, 0.0, 0.0);
198 let r_final = Vector3::new(0.0, 8000.0, 0.0);
199
200 let dm = TransferKind::Auto
201 .direction_of_motion(&r_final, &r_init)
202 .unwrap();
203
204 assert_eq!(dm, 1.0, "Auto should pick short-way for Δν=π/2; got {dm}");
205 }
206}