keplerian_sim/cached_orbit.rs
1#[cfg(feature = "serde")]
2use serde::{Deserialize, Serialize};
3
4use crate::{ApoapsisSetterError, CompactOrbit, Matrix3x2, OrbitTrait};
5
6use core::f64::consts::{PI, TAU};
7
8/// A struct representing a Keplerian orbit with some cached values.
9///
10/// This struct consumes significantly more memory because of the cache.
11/// However, this will speed up orbital calculations.
12/// If memory efficiency is your goal, you may consider using the `CompactOrbit` struct instead.
13///
14/// # Example
15/// ```
16/// use keplerian_sim::{Orbit, OrbitTrait};
17///
18/// let orbit = Orbit::new(
19/// // Initialize using eccentricity, periapsis, inclination,
20/// // argument of periapsis, longitude of ascending node,
21/// // and mean anomaly at epoch
22///
23/// // Eccentricity
24/// 0.0,
25///
26/// // Periapsis
27/// 1.0,
28///
29/// // Inclination
30/// 0.0,
31///
32/// // Argument of periapsis
33/// 0.0,
34///
35/// // Longitude of ascending node
36/// 0.0,
37///
38/// // Mean anomaly at epoch
39/// 0.0,
40///
41/// // Gravitational parameter of the parent body
42/// 1.0,
43/// );
44///
45/// let orbit = Orbit::with_apoapsis(
46/// // Initialize using apoapsis in place of eccentricity
47///
48/// // Apoapsis
49/// 2.0,
50///
51/// // Periapsis
52/// 1.0,
53///
54/// // Inclination
55/// 0.0,
56///
57/// // Argument of periapsis
58/// 0.0,
59///
60/// // Longitude of ascending node
61/// 0.0,
62///
63/// // Mean anomaly at epoch
64/// 0.0,
65///
66/// // Gravitational parameter of the parent body
67/// 1.0,
68/// );
69/// ```
70/// See [Orbit::new] and [Orbit::with_apoapsis] for more information.
71#[derive(Clone, Debug, PartialEq)]
72#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
73pub struct Orbit {
74 /// The eccentricity of the orbit.
75 /// e < 1: ellipse
76 /// e = 1: parabola
77 /// e > 1: hyperbola
78 eccentricity: f64,
79
80 /// The periapsis of the orbit, in meters.
81 periapsis: f64,
82
83 /// The inclination of the orbit, in radians.
84 inclination: f64,
85
86 /// The argument of periapsis of the orbit, in radians.
87 arg_pe: f64,
88
89 /// The longitude of ascending node of the orbit, in radians.
90 long_asc_node: f64,
91
92 /// The mean anomaly at orbit epoch, in radians.
93 mean_anomaly: f64,
94 /// The gravitational parameter of the parent body.
95 mu: f64,
96 cache: OrbitCachedCalculations,
97}
98
99// -------- MEMO --------
100// When updating this struct, please review the following methods:
101// `Orbit::get_cached_calculations()`
102// `<Orbit as OrbitTrait>::set_*()`
103#[derive(Clone, Debug, PartialEq)]
104#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
105struct OrbitCachedCalculations {
106 /// The transformation matrix to tilt the 2D planar orbit into 3D space.
107 transformation_matrix: Matrix3x2,
108}
109// Initialization and cache management
110impl Orbit {
111 /// Creates a new orbit with the given parameters.
112 ///
113 /// Note: This function uses eccentricity instead of apoapsis.
114 /// If you want to provide an apoapsis instead, consider using the
115 /// [`Orbit::with_apoapsis`] function instead.
116 ///
117 /// ### Parameters
118 /// - `eccentricity`: The eccentricity of the orbit.
119 /// - `periapsis`: The periapsis of the orbit, in meters.
120 /// - `inclination`: The inclination of the orbit, in radians.
121 /// - `arg_pe`: The argument of periapsis of the orbit, in radians.
122 /// - `long_asc_node`: The longitude of ascending node of the orbit, in radians.
123 /// - `mean_anomaly`: The mean anomaly of the orbit at epoch, in radians.
124 /// - `mu`: The gravitational parameter of the parent body, in m^3 s^-2.
125 pub fn new(
126 eccentricity: f64,
127 periapsis: f64,
128 inclination: f64,
129 arg_pe: f64,
130 long_asc_node: f64,
131 mean_anomaly: f64,
132 mu: f64,
133 ) -> Orbit {
134 let cache = Self::get_cached_calculations(inclination, arg_pe, long_asc_node);
135 Orbit {
136 eccentricity,
137 periapsis,
138 inclination,
139 arg_pe,
140 long_asc_node,
141 mean_anomaly,
142 mu,
143 cache,
144 }
145 }
146
147 /// Creates a new orbit with the given parameters.
148 ///
149 /// Note: This function uses apoapsis instead of eccentricity.
150 /// Because of this, it's not recommended to initialize
151 /// parabolic or hyperbolic 'orbits' with this function.
152 /// If you're looking to initialize a parabolic or hyperbolic
153 /// trajectory, consider using the [`Orbit::new`] function instead.
154 ///
155 /// ### Parameters
156 /// - `apoapsis`: The apoapsis of the orbit, in meters.
157 /// - `periapsis`: The periapsis of the orbit, in meters.
158 /// - `inclination`: The inclination of the orbit, in radians.
159 /// - `arg_pe`: The argument of periapsis of the orbit, in radians.
160 /// - `long_asc_node`: The longitude of ascending node of the orbit, in radians.
161 /// - `mean_anomaly`: The mean anomaly of the orbit, in radians.
162 /// - `mu`: The gravitational parameter of the parent body, in m^3 s^-2.
163 pub fn with_apoapsis(
164 apoapsis: f64,
165 periapsis: f64,
166 inclination: f64,
167 arg_pe: f64,
168 long_asc_node: f64,
169 mean_anomaly: f64,
170 mu: f64,
171 ) -> Orbit {
172 let eccentricity = (apoapsis - periapsis) / (apoapsis + periapsis);
173 Self::new(
174 eccentricity,
175 periapsis,
176 inclination,
177 arg_pe,
178 long_asc_node,
179 mean_anomaly,
180 mu,
181 )
182 }
183
184 /// Updates the cached values in the orbit struct.
185 ///
186 /// Should only be called when the following things change:
187 /// 1. Inclination
188 /// 2. Argument of Periapsis
189 /// 3. Longitude of Ascending Node
190 fn update_cache(&mut self) {
191 self.cache =
192 Self::get_cached_calculations(self.inclination, self.arg_pe, self.long_asc_node);
193 }
194
195 fn get_cached_calculations(
196 inclination: f64,
197 arg_pe: f64,
198 long_asc_node: f64,
199 ) -> OrbitCachedCalculations {
200 let transformation_matrix =
201 Self::get_transformation_matrix(inclination, arg_pe, long_asc_node);
202
203 OrbitCachedCalculations {
204 transformation_matrix,
205 }
206 }
207
208 fn get_transformation_matrix(inclination: f64, arg_pe: f64, long_asc_node: f64) -> Matrix3x2 {
209 let mut matrix = Matrix3x2::default();
210
211 let (sin_inc, cos_inc) = inclination.sin_cos();
212 let (sin_arg_pe, cos_arg_pe) = arg_pe.sin_cos();
213 let (sin_lan, cos_lan) = long_asc_node.sin_cos();
214
215 // https://downloads.rene-schwarz.com/download/M001-Keplerian_Orbit_Elements_to_Cartesian_State_Vectors.pdf
216 matrix.e11 = cos_arg_pe * cos_lan - sin_arg_pe * cos_inc * sin_lan;
217 matrix.e12 = -(sin_arg_pe * cos_lan + cos_arg_pe * cos_inc * sin_lan);
218
219 matrix.e21 = cos_arg_pe * sin_lan + sin_arg_pe * cos_inc * cos_lan;
220 matrix.e22 = cos_arg_pe * cos_inc * cos_lan - sin_arg_pe * sin_lan;
221
222 matrix.e31 = sin_arg_pe * sin_inc;
223 matrix.e32 = cos_arg_pe * sin_inc;
224
225 matrix
226 }
227}
228
229// The actual orbit position calculations
230impl OrbitTrait for Orbit {
231 fn set_apoapsis(&mut self, apoapsis: f64) -> Result<(), ApoapsisSetterError> {
232 if apoapsis < 0.0 {
233 Err(ApoapsisSetterError::ApoapsisNegative)
234 } else if apoapsis < self.periapsis {
235 Err(ApoapsisSetterError::ApoapsisLessThanPeriapsis)
236 } else {
237 self.eccentricity = (apoapsis - self.periapsis) / (apoapsis + self.periapsis);
238
239 Ok(())
240 }
241 }
242
243 fn set_apoapsis_force(&mut self, apoapsis: f64) {
244 let mut apoapsis = apoapsis;
245 if apoapsis < self.periapsis && apoapsis >= 0.0 {
246 (apoapsis, self.periapsis) = (self.periapsis, apoapsis);
247 self.arg_pe = (self.arg_pe + PI).rem_euclid(TAU);
248 self.mean_anomaly = (self.mean_anomaly + PI).rem_euclid(TAU);
249 self.update_cache();
250 }
251
252 if apoapsis < 0.0 && apoapsis > -self.periapsis {
253 // Even for hyperbolic orbits, apoapsis cannot be between 0 and -periapsis
254 // We will interpret this as an infinite apoapsis (parabolic trajectory)
255 self.eccentricity = 1.0;
256 } else {
257 self.eccentricity = (apoapsis - self.periapsis) / (apoapsis + self.periapsis);
258 }
259 }
260
261 fn get_transformation_matrix(&self) -> Matrix3x2 {
262 self.cache.transformation_matrix
263 }
264
265 #[inline]
266 fn get_eccentricity(&self) -> f64 {
267 self.eccentricity
268 }
269
270 #[inline]
271 fn get_periapsis(&self) -> f64 {
272 self.periapsis
273 }
274
275 #[inline]
276 fn get_inclination(&self) -> f64 {
277 self.inclination
278 }
279
280 #[inline]
281 fn get_arg_pe(&self) -> f64 {
282 self.arg_pe
283 }
284
285 #[inline]
286 fn get_long_asc_node(&self) -> f64 {
287 self.long_asc_node
288 }
289
290 #[inline]
291 fn get_mean_anomaly_at_epoch(&self) -> f64 {
292 self.mean_anomaly
293 }
294
295 #[inline]
296 fn get_gravitational_parameter(&self) -> f64 {
297 self.mu
298 }
299
300 #[inline]
301 fn set_eccentricity(&mut self, value: f64) {
302 self.eccentricity = value;
303 }
304 #[inline]
305 fn set_periapsis(&mut self, value: f64) {
306 self.periapsis = value;
307 }
308 fn set_inclination(&mut self, value: f64) {
309 self.inclination = value;
310 self.update_cache();
311 }
312 fn set_arg_pe(&mut self, value: f64) {
313 self.arg_pe = value;
314 self.update_cache();
315 }
316 fn set_long_asc_node(&mut self, value: f64) {
317 self.long_asc_node = value;
318 self.update_cache();
319 }
320 #[inline]
321 fn set_mean_anomaly_at_epoch(&mut self, value: f64) {
322 self.mean_anomaly = value;
323 }
324
325 fn set_gravitational_parameter(
326 &mut self,
327 gravitational_parameter: f64,
328 mode: crate::MuSetterMode,
329 ) {
330 let new_mu = gravitational_parameter;
331 match mode {
332 crate::MuSetterMode::KeepElements => {
333 self.mu = new_mu;
334 }
335 crate::MuSetterMode::KeepPositionAtTime(t) => {
336 // We need to keep the position at time t
337 // This means keeping the mean anomaly at that point, since the
338 // orbit shape does not change
339 // Current mean anomaly:
340 // M_1(t) = M_0_1 + t * sqrt(mu_1 / |a^3|)
341 //
342 // Mean anomaly after mu changes:
343 // M_2(t) = M_0_2 + t * sqrt(mu_2 / |a^3|)
344 //
345 // M_1(t) = M_2(t)
346 //
347 // We need to find M_0_2
348 //
349 // M_0_1 + t * sqrt(mu_1 / |a^3|) = M_0_2 + t * sqrt(mu_2 / |a^3|)
350 // M_0_2 + t * sqrt(mu_2 / |a^3|) = M_0_1 + t * sqrt(mu_1 / |a^3|)
351 // M_0_2 = M_0_1 + t * sqrt(mu_1 / |a^3|) - t * sqrt(mu_2 / |a^3|)
352 // M_0_2 = M_0_1 + t * (sqrt(mu_1 / |a^3|) - sqrt(mu_2 / |a^3|))
353 let inv_abs_a_cubed = self.get_semi_major_axis().powi(3).abs().recip();
354
355 self.mean_anomaly +=
356 t * ((self.mu * inv_abs_a_cubed).sqrt() - (new_mu * inv_abs_a_cubed).sqrt());
357
358 self.mu = new_mu;
359 }
360 crate::MuSetterMode::KeepKnownStateVectors {
361 state_vectors,
362 time,
363 } => {
364 let new = state_vectors.to_cached_orbit(new_mu, time);
365 *self = new;
366 }
367 crate::MuSetterMode::KeepStateVectorsAtTime(time) => {
368 let ecc_anom = self.get_eccentric_anomaly_at_time(time);
369 let state_vectors = self.get_state_vectors_at_eccentric_anomaly(ecc_anom);
370 let new = state_vectors.to_cached_orbit(new_mu, time);
371 *self = new;
372 }
373 }
374 }
375}
376
377impl From<CompactOrbit> for Orbit {
378 fn from(compact: CompactOrbit) -> Self {
379 Self::new(
380 compact.eccentricity,
381 compact.periapsis,
382 compact.inclination,
383 compact.arg_pe,
384 compact.long_asc_node,
385 compact.mean_anomaly,
386 compact.mu,
387 )
388 }
389}
390
391impl Default for Orbit {
392 /// Creates a unit orbit.
393 ///
394 /// The unit orbit is a perfect circle of radius 1 and no "tilt".
395 fn default() -> Orbit {
396 Self::new(0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0)
397 }
398}