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}