1use lox_core::coords::Cartesian;
6use lox_time::Time;
7use lox_time::deltas::TimeDelta;
8use lox_time::intervals::TimeInterval;
9use lox_time::time_scales::{DynTimeScale, TimeScale};
10use thiserror::Error;
11
12use lox_bodies::{DynOrigin, Origin, PointMass, TryPointMass, UndefinedOriginPropertyError};
13
14use crate::orbits::{CartesianOrbit, TrajectorError, Trajectory};
15use crate::propagators::{Propagator, stumpff};
16use lox_frames::{
17 DynFrame, NonQuasiInertialFrameError, QuasiInertial, ReferenceFrame, TryQuasiInertial,
18};
19
20#[derive(Debug, Error)]
22pub enum ValladoError {
23 #[error("did not converge")]
25 NotConverged,
26 #[error(transparent)]
28 TrajectoryError(#[from] TrajectorError),
29 #[error(transparent)]
31 UndefinedOriginProperty(#[from] UndefinedOriginPropertyError),
32 #[error(transparent)]
34 NonQuasiInertialFrame(#[from] NonQuasiInertialFrameError),
35}
36
37#[derive(Debug, Copy, Clone, PartialEq)]
39pub struct Vallado<T: TimeScale, O: Origin, R: ReferenceFrame> {
40 initial_state: CartesianOrbit<T, O, R>,
41 max_iter: i32,
42 step: Option<TimeDelta>,
43}
44
45pub type DynVallado = Vallado<DynTimeScale, DynOrigin, DynFrame>;
47
48impl<T, O, R> Vallado<T, O, R>
50where
51 T: TimeScale,
52 O: PointMass + Copy,
53 R: QuasiInertial,
54{
55 pub fn new(initial_state: CartesianOrbit<T, O, R>) -> Self {
57 Self {
58 initial_state,
59 max_iter: 300,
60 step: None,
61 }
62 }
63}
64
65impl<T, O, R> Vallado<T, O, R>
67where
68 T: TimeScale,
69 O: TryPointMass + Copy,
70 R: TryQuasiInertial + Copy,
71{
72 pub fn try_new(initial_state: CartesianOrbit<T, O, R>) -> Result<Self, ValladoError> {
74 initial_state.origin().try_gravitational_parameter()?;
75 initial_state.reference_frame().try_quasi_inertial()?;
76 Ok(Self {
77 initial_state,
78 max_iter: 300,
79 step: None,
80 })
81 }
82}
83
84impl<T, O, R> Vallado<T, O, R>
85where
86 T: TimeScale,
87 O: TryPointMass + Copy,
88 R: ReferenceFrame,
89{
90 fn gravitational_parameter(&self) -> f64 {
91 self.initial_state
92 .origin()
93 .try_gravitational_parameter()
94 .expect("gravitational parameter should be available")
95 .as_f64()
96 }
97
98 pub fn with_max_iter(mut self, max_iter: i32) -> Self {
100 self.max_iter = max_iter;
101 self
102 }
103
104 pub fn max_iter(&self) -> i32 {
106 self.max_iter
107 }
108
109 pub fn with_step(mut self, step: TimeDelta) -> Self {
111 self.step = Some(step);
112 self
113 }
114
115 pub fn initial_state(&self) -> &CartesianOrbit<T, O, R> {
117 &self.initial_state
118 }
119
120 pub fn origin(&self) -> O {
122 self.initial_state.origin()
123 }
124
125 pub fn reference_frame(&self) -> R
127 where
128 R: Copy,
129 {
130 self.initial_state.reference_frame()
131 }
132
133 pub fn state_at(&self, time: Time<T>) -> Result<CartesianOrbit<T, O, R>, ValladoError>
135 where
136 T: Copy,
137 R: Copy,
138 {
139 let frame = self.reference_frame();
140 let origin = self.origin();
141 let mu = self.gravitational_parameter();
142 let t0 = self.initial_state.time();
143 let dt = (time - t0).to_seconds().to_f64();
144 let sqrt_mu = mu.sqrt();
145 let p0 = self.initial_state.position();
146 let v0 = self.initial_state.velocity();
147 let dot_p0v0 = p0.dot(v0);
148 let norm_p0 = p0.length();
149 let alpha = -v0.dot(v0) / mu + 2.0 / norm_p0;
150
151 let mut xi_new = if alpha > 0.0 {
152 sqrt_mu * dt * alpha
153 } else if alpha < 0.0 {
154 dt.signum()
155 * (-1.0 / alpha).powf(0.5)
156 * (-2.0 * mu * alpha * dt
157 / (dot_p0v0 + dt.signum() * (-mu / alpha).sqrt() * (1.0 - norm_p0 * alpha)))
158 .ln()
159 } else {
160 sqrt_mu * dt / norm_p0
161 };
162
163 let mut count = 0;
164 while count < self.max_iter {
165 let xi = xi_new;
166 let psi = xi * xi * alpha;
167 let c2_psi = stumpff::c2(psi);
168 let c3_psi = stumpff::c3(psi);
169 let norm_r = xi.powi(2) * c2_psi
170 + dot_p0v0 / sqrt_mu * xi * (1.0 - psi * c3_psi)
171 + norm_p0 * (1.0 - psi * c2_psi);
172 let delta_xi = (sqrt_mu * dt
173 - xi.powi(3) * c3_psi
174 - dot_p0v0 / sqrt_mu * xi.powi(2) * c2_psi
175 - norm_p0 * xi * (1.0 - psi * c3_psi))
176 / norm_r;
177 xi_new = xi + delta_xi;
178 if (xi_new - xi).abs() < 1e-7 {
179 let f = 1.0 - xi.powi(2) / norm_p0 * c2_psi;
180 let g = dt - xi.powi(3) / sqrt_mu * c3_psi;
181
182 let gdot = 1.0 - xi.powi(2) / norm_r * c2_psi;
183 let fdot = sqrt_mu / (norm_r * norm_p0) * xi * (psi * c3_psi - 1.0);
184
185 debug_assert!((f * gdot - fdot * g - 1.0).abs() < 1e-5);
186
187 let p = f * p0 + g * v0;
188 let v = fdot * p0 + gdot * v0;
189
190 return Ok(CartesianOrbit::new(
191 Cartesian::from_vecs(p, v),
192 time,
193 origin,
194 frame,
195 ));
196 } else {
197 count += 1
198 }
199 }
200 Err(ValladoError::NotConverged)
201 }
202}
203
204impl<T, O, R> Propagator<T, O> for Vallado<T, O, R>
206where
207 T: TimeScale + Copy + PartialOrd,
208 O: TryPointMass + Copy,
209 R: ReferenceFrame + Copy,
210{
211 type Frame = R;
212 type Error = ValladoError;
213
214 fn state_at(&self, time: Time<T>) -> Result<CartesianOrbit<T, O, R>, ValladoError> {
215 self.state_at(time)
216 }
217
218 fn propagate(&self, interval: TimeInterval<T>) -> Result<Trajectory<T, O, R>, ValladoError> {
219 let step = self.step.unwrap_or(TimeDelta::from_seconds(1));
220 let states = interval
221 .step_by(step)
222 .map(|t| self.state_at(t))
223 .collect::<Result<Vec<_>, _>>()?;
224 Ok(Trajectory::try_new(states)?)
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use lox_bodies::Earth;
231 use lox_core::elements::Keplerian as CoreKeplerian;
232 use lox_core::units::{AngleUnits, DistanceUnits};
233 use lox_frames::Icrf;
234 use lox_test_utils::assert_approx_eq;
235 use lox_time::intervals::Interval;
236 use lox_time::time_scales::Tdb;
237 use lox_time::utc;
238 use lox_time::utc::Utc;
239
240 use super::*;
241
242 #[test]
243 fn test_vallado_state_at() {
244 let utc = utc!(2023, 3, 25, 21, 8, 0.0).unwrap();
245 let time = utc.to_time().to_scale(Tdb);
246 let semi_major = 24464.560;
247 let eccentricity = 0.7311;
248 let inclination = 0.122138;
249 let ascending_node = 1.00681;
250 let periapsis_arg = 3.10686;
251 let true_anomaly = 0.44369564302687126;
252
253 let k0 = CoreKeplerian::builder()
254 .with_semi_major_axis(semi_major.km(), eccentricity)
255 .with_inclination(inclination.rad())
256 .with_longitude_of_ascending_node(ascending_node.rad())
257 .with_argument_of_periapsis(periapsis_arg.rad())
258 .with_true_anomaly(true_anomaly.rad())
259 .build()
260 .unwrap();
261
262 let mu = Earth.gravitational_parameter();
263 let s0 = CartesianOrbit::new(k0.to_cartesian(mu), time, Earth, Icrf);
264 let period = k0.orbital_period(mu).unwrap();
265 let t1 = time + period;
266
267 let propagator = Vallado::new(s0);
268 let s1 = propagator.state_at(t1).expect("propagator should converge");
269
270 let k1 = s1.to_keplerian();
271 assert_approx_eq!(
272 k1.semi_major_axis().as_f64(),
273 semi_major.km().as_f64(),
274 rtol <= 1e-8
275 );
276 assert_approx_eq!(k1.eccentricity().as_f64(), eccentricity, rtol <= 1e-8);
277 assert_approx_eq!(k1.inclination().as_f64(), inclination, rtol <= 1e-8);
278 assert_approx_eq!(
279 k1.longitude_of_ascending_node().as_f64(),
280 ascending_node,
281 rtol <= 1e-8
282 );
283 assert_approx_eq!(
284 k1.argument_of_periapsis().as_f64(),
285 periapsis_arg,
286 rtol <= 1e-8
287 );
288 assert_approx_eq!(k1.true_anomaly().as_f64(), true_anomaly, rtol <= 1e-8);
289 assert_approx_eq!(k1.time(), t1);
290 }
291
292 #[test]
293 fn test_vallado_propagate() {
294 let utc = utc!(2023, 3, 25, 21, 8, 0.0).unwrap();
295 let time = utc.to_time().to_scale(Tdb);
296 let semi_major = 24464.560;
297 let eccentricity = 0.7311;
298 let inclination = 0.122138;
299 let ascending_node = 1.00681;
300 let periapsis_arg = 3.10686;
301 let true_anomaly = 0.44369564302687126;
302
303 let k0 = CoreKeplerian::builder()
304 .with_semi_major_axis(semi_major.km(), eccentricity)
305 .with_inclination(inclination.rad())
306 .with_longitude_of_ascending_node(ascending_node.rad())
307 .with_argument_of_periapsis(periapsis_arg.rad())
308 .with_true_anomaly(true_anomaly.rad())
309 .build()
310 .unwrap();
311
312 let mu = Earth.gravitational_parameter();
313 let s0 = CartesianOrbit::new(k0.to_cartesian(mu), time, Earth, Icrf);
314 let period = k0.orbital_period(mu).unwrap();
315 let t_end = time + period;
316 let interval = Interval::new(time, t_end);
317 let trajectory = Vallado::new(s0).propagate(interval).unwrap();
318 let s1 = trajectory.interpolate(period);
319 let k1 = s1.to_keplerian();
320
321 assert_approx_eq!(
322 k1.semi_major_axis().as_f64(),
323 semi_major.km().as_f64(),
324 rtol <= 1e-8
325 );
326 assert_approx_eq!(k1.eccentricity().as_f64(), eccentricity, rtol <= 1e-8);
327 assert_approx_eq!(k1.inclination().as_f64(), inclination, rtol <= 1e-8);
328 assert_approx_eq!(
329 k1.longitude_of_ascending_node().as_f64(),
330 ascending_node,
331 rtol <= 1e-8
332 );
333 assert_approx_eq!(
334 k1.argument_of_periapsis().as_f64(),
335 periapsis_arg,
336 rtol <= 1e-8
337 );
338 assert_approx_eq!(k1.true_anomaly().as_f64(), true_anomaly, rtol <= 1e-8);
339 }
340
341 #[test]
342 fn test_try_new_with_static_types() {
343 let utc = utc!(2023, 3, 25, 21, 8, 0.0).unwrap();
344 let time = utc.to_time().to_scale(Tdb);
345 let pos = glam::DVec3::new(-1076225.32, -6765896.36, -332308.78);
346 let vel = glam::DVec3::new(9356.86, -3312.35, -1188.02);
347 let s0 = CartesianOrbit::new(
348 lox_core::coords::Cartesian::from_vecs(pos, vel),
349 time,
350 Earth,
351 Icrf,
352 );
353 let vallado = Vallado::try_new(s0);
355 assert!(vallado.is_ok());
356 }
357
358 #[test]
359 fn test_try_new_rejects_non_point_mass() {
360 use lox_bodies::DynOrigin;
361
362 let utc = utc!(2023, 3, 25, 21, 8, 0.0).unwrap();
363 let time = utc.to_dyn_time();
364 let pos = glam::DVec3::new(-1076225.32, -6765896.36, -332308.78);
365 let vel = glam::DVec3::new(9356.86, -3312.35, -1188.02);
366 let s0 = CartesianOrbit::new(
367 lox_core::coords::Cartesian::from_vecs(pos, vel),
368 time,
369 DynOrigin::Callirrhoe,
370 DynFrame::Icrf,
371 );
372 let result = Vallado::try_new(s0);
373 assert!(result.is_err());
374 }
375
376 #[test]
377 fn test_try_new_rejects_non_inertial_frame() {
378 use lox_bodies::DynOrigin;
379
380 let utc = utc!(2023, 3, 25, 21, 8, 0.0).unwrap();
381 let time = utc.to_dyn_time();
382 let pos = glam::DVec3::new(-1076225.32, -6765896.36, -332308.78);
383 let vel = glam::DVec3::new(9356.86, -3312.35, -1188.02);
384 let s0 = CartesianOrbit::new(
385 lox_core::coords::Cartesian::from_vecs(pos, vel),
386 time,
387 DynOrigin::Earth,
388 DynFrame::Iau(DynOrigin::Earth),
389 );
390 let result = Vallado::try_new(s0);
391 assert!(result.is_err());
392 }
393}