keplerian_sim/
lib.rs

1//! # Keplerian Orbital Mechanics
2//! This library crate contains logic for Keplerian orbits, similar to the ones
3//! you'd find in a game like Kerbal Space Program.  
4//!
5//! Keplerian orbits are special in that they are more stable and predictable than
6//! Newtonian orbits. In fact, unlike Newtonian orbits, Keplerian orbits don't use
7//! time steps to calculate the next position of an object. Keplerian orbits use
8//! state vectors to determine the object's *full trajectory* at any given time.  
9//! This way, you don't need to worry about lag destabilizing Keplerian orbits.  
10//!
11//! However, Keplerian orbits are significantly more complex to calculate than
12//! just using Newtonian physics. It's also a two-body simulation, meaning that
13//! it doesn't account for external forces like gravity from other bodies or the
14//! engines of a spacecraft.
15//!
16//! The way Kerbal Space Program handles this is to have an "on-rails" physics
17//! system utilizing Keplerian orbits, and an "active" physics system utilizing
18//! Newtonian two-body physics.
19//!
20//! ## Getting started
21//! This crate provides four main structs:
22//! - [`Orbit`]: A struct representing an orbit around a celestial body.
23//!   Each instance of this struct has some cached data to speed up
24//!   certain calculations, and has a larger memory footprint.
25//! - [`CompactOrbit`]: A struct representing an orbit around a celestial body.
26//!   This struct has a smaller memory footprint than the regular `Orbit` struct,
27//!   but some calculations may take 2~10x slower because it doesn't save any
28//!   cached calculations.
29//!
30//! We used to have `body`, `universe`, and `body_presets` modules, however these
31//! were removed from the main library because some programs have different
32//! needs on what to store on each body. The code was moved to the `simulate`
33//! example file in the repository:
34//! <https://github.com/Not-A-Normal-Robot/keplerian-sim/blob/0d60ed756dc6b09c60d779167cfa0e3346e09213/examples/simulate.rs>
35//!
36//! ## Example
37//!
38//! ```rust
39//! use glam::DVec3;
40//!
41//! use keplerian_sim::{Orbit, OrbitTrait};
42//!
43//! # fn main() {
44//! // Create a perfectly circular orbit with a radius of 1 meter
45//! let orbit = Orbit::default();
46//! assert_eq!(orbit.get_position_at_time(0.0), DVec3::new(1.0, 0.0, 0.0));
47//! # }
48//! #
49//! ```
50
51#![warn(missing_docs)]
52#![forbid(unsafe_code)]
53
54mod cached_orbit;
55mod compact_orbit;
56
57use std::f64::consts::{PI, TAU};
58
59pub use cached_orbit::Orbit;
60pub use compact_orbit::CompactOrbit;
61use glam::{DVec2, DVec3};
62
63#[cfg(feature = "serde")]
64use serde::{Deserialize, Serialize};
65
66/// A constant used to get the initial seed for the eccentric anomaly.
67///
68/// It's very arbitrary, but according to some testing, a value just
69/// below 1 works better than exactly 1.
70///
71/// Source:
72/// "Two fast and accurate routines for solving the elliptic Kepler
73/// equation for all values of the eccentricity and mean anomaly"
74/// by Daniele Tommasini and David N. Olivieri,
75/// section 2.1.2, 'The "rational seed"'
76///
77/// <https://doi.org/10.1051/0004-6361/202141423>
78const B: f64 = 0.999999;
79
80/// A constant used for the Laguerre method.
81///
82/// The paper "An improved algorithm due to
83/// laguerre for the solution of Kepler's equation."
84/// says:
85///
86/// > Similar experimentation has been done with values of n both greater and smaller
87/// > than n = 5. The speed of convergence seems to be very insensitive to the choice of n.
88/// > No value of n was found to yield consistently better convergence properties than the
89/// > choice of n = 5 though specific cases were found where other choices would give
90/// > faster convergence.
91const N_U32: u32 = 5;
92
93/// A constant used for the Laguerre method.
94///
95/// The paper "An improved algorithm due to
96/// laguerre for the solution of Kepler's equation."
97/// says:
98///
99/// > Similar experimentation has been done with values of n both greater and smaller
100/// > than n = 5. The speed of convergence seems to be very insensitive to the choice of n.
101/// > No value of n was found to yield consistently better convergence properties than the
102/// > choice of n = 5 though specific cases were found where other choices would give
103/// > faster convergence.
104const N_F64: f64 = N_U32 as f64;
105
106/// The maximum number of iterations for the numerical approach algorithms.
107///
108/// This is used to prevent infinite loops in case the method fails to converge.
109const NUMERIC_MAX_ITERS: u32 = 1000;
110
111const PI_SQUARED: f64 = PI * PI;
112
113/// A struct representing a 3x2 matrix.
114///
115/// This struct is used to store the transformation matrix
116/// for transforming a 2D vector into a 3D vector.
117///
118/// Namely, it is used in the [`transform_pqw_vector`][OrbitTrait::transform_pqw_vector]
119/// method to tilt a 2D position into 3D, using the orbital parameters.
120///
121/// Each element is named `eXY`, where `X` is the row and `Y` is the column.
122///
123/// # Example
124/// ```
125/// use glam::{DVec2, DVec3};
126///
127/// use keplerian_sim::Matrix3x2;
128///
129/// let matrix = Matrix3x2 {
130///    e11: 1.0, e12: 0.0,
131///    e21: 0.0, e22: 1.0,
132///    e31: 0.0, e32: 0.0,
133/// };
134///
135/// let vec = DVec2::new(1.0, 2.0);
136///
137/// let result = matrix.dot_vec(vec);
138///
139/// assert_eq!(result, DVec3::new(1.0, 2.0, 0.0));
140/// ```
141#[allow(missing_docs)]
142#[derive(Copy, Clone, Debug, Default, PartialEq)]
143#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
144pub struct Matrix3x2 {
145    // Element XY
146    pub e11: f64,
147    pub e12: f64,
148    pub e21: f64,
149    pub e22: f64,
150    pub e31: f64,
151    pub e32: f64,
152}
153
154impl Matrix3x2 {
155    /// The zero matrix.
156    ///
157    /// Multiplying a vector with this results in a vector
158    /// of zero length.
159    pub const ZERO: Self = Self {
160        e11: 0.0,
161        e12: 0.0,
162        e21: 0.0,
163        e22: 0.0,
164        e31: 0.0,
165        e32: 0.0,
166    };
167
168    /// The identity matrix.
169    ///
170    /// Multiplying a vector with this results in the same
171    /// vector.
172    pub const IDENTITY: Self = Self {
173        e11: 1.0,
174        e22: 1.0,
175        ..Self::ZERO
176    };
177
178    /// Computes a dot product between this matrix and a 2D vector.
179    ///
180    /// # Example
181    /// ```
182    /// use glam::{DVec2, DVec3};
183    ///
184    /// use keplerian_sim::Matrix3x2;
185    ///
186    /// let matrix = Matrix3x2 {
187    ///     e11: 1.0, e12: 0.0,
188    ///     e21: 0.0, e22: 1.0,
189    ///     e31: 1.0, e32: 1.0,
190    /// };
191    ///
192    /// let vec = DVec2::new(1.0, 2.0);
193    ///
194    /// let result = matrix.dot_vec(vec);
195    ///
196    /// assert_eq!(result, DVec3::new(1.0, 2.0, 3.0));
197    /// ```
198    pub fn dot_vec(&self, vec: DVec2) -> DVec3 {
199        DVec3::new(
200            vec.x * self.e11 + vec.y * self.e12,
201            vec.x * self.e21 + vec.y * self.e22,
202            vec.x * self.e31 + vec.y * self.e32,
203        )
204    }
205}
206
207#[cfg(feature = "mint")]
208impl From<Matrix3x2> for mint::RowMatrix3x2<f64> {
209    fn from(value: Matrix3x2) -> Self {
210        mint::RowMatrix3x2::<f64> {
211            x: mint::Vector2::<f64> {
212                x: value.e11,
213                y: value.e12,
214            },
215            y: mint::Vector2::<f64> {
216                x: value.e21,
217                y: value.e22,
218            },
219            z: mint::Vector2::<f64> {
220                x: value.e31,
221                y: value.e32,
222            },
223        }
224    }
225}
226
227#[cfg(feature = "mint")]
228impl mint::IntoMint for Matrix3x2 {
229    type MintType = mint::RowMatrix3x2<f64>;
230}
231
232#[cfg(feature = "mint")]
233impl From<mint::RowMatrix3x2<f64>> for Matrix3x2 {
234    fn from(value: mint::RowMatrix3x2<f64>) -> Self {
235        Self {
236            e11: value.x.x,
237            e12: value.x.y,
238            e21: value.y.x,
239            e22: value.y.y,
240            e31: value.z.x,
241            e32: value.z.y,
242        }
243    }
244}
245
246/// A struct representing a position and velocity at a point in the orbit.
247///
248/// The position and velocity vectors are three-dimensional.
249///
250/// The position vector is in meters, while the velocity vector is in
251/// meters per second.
252///
253/// State vectors can be used to form an orbit, see
254/// [`to_compact_orbit`][Self::to_compact_orbit] and
255/// [`to_cached_orbit`][Self::to_cached_orbit]
256/// for more information.
257#[derive(Clone, Copy, Debug, PartialEq)]
258#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
259pub struct StateVectors {
260    /// The 3D position at a point in the orbit, in meters.
261    pub position: DVec3,
262    /// The 3D velocity at a point in the orbit, in meters per second.
263    pub velocity: DVec3,
264}
265
266impl StateVectors {
267    /// Create a new [`CompactOrbit`] struct from the state
268    /// vectors and a given mu value.
269    ///
270    /// # Mu
271    /// Mu is also known as the gravitational parameter, and
272    /// is equal to `GM`, where `G` is the gravitational constant,
273    /// and `M` is the mass of the parent body.  
274    /// It can be described as how strongly the parent body pulls on
275    /// the orbiting body.
276    ///
277    /// Learn more about the gravitational parameter:
278    /// <https://en.wikipedia.org/wiki/Standard_gravitational_parameter>
279    ///
280    /// # Time
281    /// The time passed into the function is measured in seconds.
282    ///
283    /// # Performance
284    /// This function is not too performant as it uses several trigonometric operations.  
285    ///
286    /// For single conversions, this is faster than
287    /// [the cached orbit converter][Self::to_cached_orbit].  
288    /// However, consider using the cached orbit instead if you want to use the same orbit for
289    /// many calculations, as the caching speed benefits should outgrow the small initialization
290    /// overhead.
291    ///
292    /// # Reference Frame
293    /// This function expects a state vector where the position's origin (0.0, 0.0, 0.0)
294    /// is the center of the parent body.
295    ///
296    /// # Parabolic Support
297    /// This function does not yet support parabolic trajectories.
298    /// Non-finite values may be returned for such cases.
299    ///
300    /// # Constraints
301    /// The position must not be at the origin, and the velocity must not be at zero.  
302    /// If this constraint is breached, you may get invalid values such as infinities
303    /// or NaNs.
304    ///
305    /// # Examples
306    /// Simple use-case:
307    /// ```
308    /// use keplerian_sim::{CompactOrbit, OrbitTrait};
309    ///
310    /// let orbit = CompactOrbit::default();
311    /// let mu = orbit.get_gravitational_parameter();
312    /// let time = 0.0;
313    ///
314    /// let sv = orbit.get_state_vectors_at_time(time);
315    ///
316    /// let new_orbit = sv.to_compact_orbit(mu, time);
317    ///
318    /// assert_eq!(orbit.get_eccentricity(), new_orbit.get_eccentricity());
319    /// assert_eq!(orbit.get_periapsis(), new_orbit.get_periapsis());
320    /// ```
321    /// To simulate an instantaneous 0.1 m/s prograde burn at periapsis:
322    /// ```
323    /// use keplerian_sim::{CompactOrbit, OrbitTrait, StateVectors};
324    /// use glam::DVec3;
325    ///
326    /// let orbit = CompactOrbit::default();
327    /// let mu = orbit.get_gravitational_parameter();
328    /// let time = 0.0;
329    ///
330    /// let sv = orbit.get_state_vectors_at_time(time);
331    /// assert_eq!(
332    ///     sv,
333    ///     StateVectors {
334    ///         position: DVec3::new(1.0, 0.0, 0.0),
335    ///         velocity: DVec3::new(0.0, 1.0, 0.0),
336    ///     }
337    /// );
338    ///
339    /// let new_sv = StateVectors {
340    ///     velocity: sv.velocity + DVec3::new(0.0, 0.1, 0.0),
341    ///     ..sv
342    /// };
343    ///
344    /// let new_orbit = new_sv.to_compact_orbit(mu, time);
345    ///
346    /// assert_eq!(
347    ///     new_orbit,
348    ///     CompactOrbit::new(
349    ///         0.2100000000000002, // eccentricity
350    ///         1.0, // periapsis
351    ///         0.0, // inclination
352    ///         0.0, // argument of periapsis
353    ///         0.0, // longitude of ascending node
354    ///         0.0, // mean anomaly
355    ///         1.0, // gravitational parameter
356    ///     )
357    /// )
358    /// ```
359    #[must_use]
360    pub fn to_compact_orbit(self, mu: f64, time: f64) -> CompactOrbit {
361        // Reference:
362        // https://orbital-mechanics.space/classical-orbital-elements/orbital-elements-and-the-state-vector.html
363        // Note: That site doesn't use the same "base elements" and
364        // conversions will need to be done at the end
365
366        // Precalculated values
367        let altitude = self.position.length();
368        let altitude_recip = altitude.recip();
369        let position_normal = self.position * altitude_recip;
370        let mu_recip = mu.recip();
371
372        // Step 1: Position and Velocity Magnitudes (i.e. speeds)
373        let radial_speed = self.velocity.dot(position_normal);
374
375        // Step 2: Orbital Angular Momentum
376        let angular_momentum_vector = self.position.cross(self.velocity);
377        let angular_momentum = angular_momentum_vector.length();
378
379        // Step 3: Inclination
380        let inclination = (angular_momentum_vector.z / angular_momentum).acos();
381
382        // Step 4: Right Ascension of the Ascending Node
383        // Here we use René Schwarz's simplification of the cross product
384        // between (0, 0, 1) and the angular momentum vector, outlined in:
385        // https://downloads.rene-schwarz.com/download/M002-Cartesian_State_Vectors_to_Keplerian_Orbit_Elements.pdf
386        let asc_vec2 = DVec2::new(-angular_momentum_vector.y, angular_momentum_vector.x);
387        let asc_len = asc_vec2.length();
388        let asc_len_recip = asc_len.recip();
389        let asc_vec3 = asc_vec2.extend(0.0);
390
391        let long_asc_node = {
392            let tmp = (asc_vec3.x * asc_len_recip).acos();
393            if asc_vec3.y >= 0.0 {
394                tmp
395            } else {
396                TAU - tmp
397            }
398        };
399        // LAN is undefined in equatorial orbits (i = 0)
400        // Instead of returning NaN and ruining everything
401        // we'll set it to zero
402        let equatorial = long_asc_node.is_nan();
403        let long_asc_node = if equatorial { 0.0 } else { long_asc_node };
404
405        // Step 5: Eccentricity
406        let eccentricity_vector =
407            self.velocity.cross(angular_momentum_vector) * mu_recip - position_normal;
408        let eccentricity = eccentricity_vector.length();
409        let eccentricity_recip = eccentricity.recip();
410        let circular = eccentricity < 1e-6;
411
412        // Step 6: Argument of Periapsis
413        // In equatorial orbits, argument of periapsis is undefined because
414        // the longitude of ascending node is undefined
415        // However, the longitude of periapsis is defined.
416        // Since longitude of periapsis = argument of periapsis + longitude of ascending node,
417        // and we set longitude of ascending node to 0.0,
418        // we can just set the argument of periapsis to the longitude of periapsis.
419
420        // ASRI_306 on https://space.stackexchange.com/a/38316/ says (paraphrased):
421        //
422        //  If it is not circular but equatorial then,
423        //      cos(arg_pe_true) = e_x / ||e||
424        //  If it is circular but inclined then,
425        //      cos(arg_pe) = (n . r) / (||n|| ||r||)
426        //  If it is circular and equatorial then,
427        //      cos(arg_pe_true) = r_x / ||r||
428        //
429        // I'm assuming the "arg_pe_true" means the longitude of periapsis,
430        // which would be equal to the argument of periapsis when the
431        // longitude of ascending node is zero (which it is in this case).
432        let arg_pe = match (circular, equatorial) {
433            (false, false) => {
434                // Not circular, not equatorial
435                // Use normal equation
436                let tmp =
437                    (eccentricity_vector.dot(asc_vec3) * eccentricity_recip * asc_len_recip).acos();
438                if eccentricity_vector.z >= 0.0 {
439                    tmp
440                } else {
441                    TAU - tmp
442                }
443            }
444            (false, true) => {
445                // Not circular, equatorial: longitude of periapsis = acos(e_x/|e|) with sign from y
446                let tmp = (eccentricity_vector.x * eccentricity_recip).acos();
447
448                // Acos only returns values in [0, pi] instead of [0, 2pi]. To recover the
449                // full range, we do a sign check on `e.y`, similar to the normal equation earlier,
450                // except since the orbit lies on the XY plane we use the Y component instead of Z
451                if eccentricity_vector.y >= 0.0 {
452                    tmp
453                } else {
454                    TAU - tmp
455                }
456            }
457            (true, false) => {
458                // Circular, not equatorial
459
460                // Since the orbit is symmetrical, it's fine to only have arg_pe in the
461                // [0, pi] range. We will adjust for this discrepancy in the true anomaly
462                // calculation.
463                (asc_vec3.dot(self.position) * altitude_recip * asc_len_recip).acos()
464            }
465            (true, true) => {
466                // Circular and equatorial
467                // This is a very weird case, so we just set it to zero and adjust
468                // for this discrepancy in the true anomaly calculation instead.
469                0.0
470            }
471        };
472
473        fn stably_get_true_anomaly(
474            position: DVec3,
475            inclination: f64,
476            arg_pe: f64,
477            long_asc_node: f64,
478        ) -> f64 {
479            // The normal equation does not work sometimes, especially when the orbit is circular,
480            // so we get it manually by getting the P and Q basis vectors in the PQW coordinate system
481            // (see https://en.wikipedia.org/wiki/Perifocal_coordinate_system),
482            // then measuring the angle between that and our orbit using the dot product.
483            //
484            // Consider this excerpt from the transformation matrix getter from
485            // another part of the codebase:
486            //
487            // matrix.e11 = cos_arg_pe * cos_lan - sin_arg_pe * cos_inc * sin_lan;
488            // matrix.e12 = -(sin_arg_pe * cos_lan + cos_arg_pe * cos_inc * sin_lan);
489            // matrix.e21 = cos_arg_pe * sin_lan + sin_arg_pe * cos_inc * cos_lan;
490            // matrix.e22 = cos_arg_pe * cos_inc * cos_lan - sin_arg_pe * sin_lan;
491            // matrix.e31 = sin_arg_pe * sin_inc;
492            // matrix.e32 = cos_arg_pe * sin_inc;
493            //
494            // Here, `matrix.e*1` (namely e11, e21, e31) describes the P basis vector,
495            // meanwhile `matrix.e*2` describes the Q basis vector.
496
497            let (sin_inc, cos_inc) = inclination.sin_cos();
498            let (sin_arg_pe, cos_arg_pe) = arg_pe.sin_cos();
499            let (sin_lan, cos_lan) = long_asc_node.sin_cos();
500
501            let p_x = cos_arg_pe * cos_lan - sin_arg_pe * cos_inc * sin_lan;
502            let p_y = cos_arg_pe * sin_lan + sin_arg_pe * cos_inc * cos_lan;
503            let p_z = sin_arg_pe * sin_inc;
504
505            let p = DVec3::new(p_x, p_y, p_z);
506
507            let q_x = -(sin_arg_pe * cos_lan + cos_arg_pe * cos_inc * sin_lan);
508            let q_y = cos_arg_pe * cos_inc * cos_lan - sin_arg_pe * sin_lan;
509            let q_z = cos_arg_pe * sin_inc;
510
511            let q = DVec3::new(q_x, q_y, q_z);
512
513            // Now that we have the P and Q basis vectors (of length 1), we can
514            // project our position into the PQW reference frame
515            let pos_p = position.dot(p);
516            let pos_q = position.dot(q);
517
518            // Then we can get the angle between the projected position and
519            // the +X direction (or technically +P here because it's projected),
520            // and since that direction points to the periapsis, that angle
521            // is the true anomaly
522            pos_q.atan2(pos_p).rem_euclid(TAU)
523        }
524
525        // Step 7: True anomaly
526        // The true anomaly is the angle from periapsis to the current position.
527        let true_anomaly = if circular | equatorial {
528            stably_get_true_anomaly(self.position, inclination, arg_pe, long_asc_node)
529        } else {
530            // Use the regular method from orbital-mechanics.space
531            // as previously mentioned
532            let tmp =
533                (eccentricity_vector.dot(self.position) * eccentricity_recip * altitude_recip)
534                    .acos();
535            if tmp.is_nan() {
536                stably_get_true_anomaly(self.position, inclination, arg_pe, long_asc_node)
537            } else if radial_speed >= 0.0 {
538                tmp
539            } else {
540                TAU - tmp
541            }
542        };
543
544        // Now we convert those elements into our desired form
545        // First we need to convert `h` (Orbital Angular Momentum)
546        // into periapsis altitude, then we need to convert the true anomaly
547        // to a mean anomaly, or to a "time at periapsis" value for parabolic
548        // orbits (not implemented yet).
549        // TODO: PARABOLIC SUPPORT: Implement that "time at periapsis" value
550
551        // Part 1: Converting orbital angular momentum into periapsis altitude
552        //
553        // https://faculty.fiu.edu/~vanhamme/ast3213/orbits.pdf says:
554        // r = (h^2 / mu) / (1 + e * cos(theta))
555        // ...where:
556        // r = altitude at a certain true anomaly in the orbit (theta)
557        // h = the orbital angular momentum scalar vale
558        // mu = the gravitational parameter
559        // e = the eccentricity
560        // theta = the true anomaly
561        //
562        // https://en.wikipedia.org/wiki/True_anomaly says:
563        // [The true anomaly] is the angle between the direction of
564        // periapsis and the current position of the body [...]
565        //
566        // This means that a true anomaly of zero means periapsis.
567        // So if we substitute zero into theta into the earlier equation...
568        //
569        // r = (h^2 / mu) / (1 + e * cos(0))
570        //   = (h^2 / mu) / (1 + e * 1)
571        //   = (h^2 / mu) / (1 + e)
572        //
573        // This gives us the altitude at true anomaly 0 (periapsis).
574        let periapsis = (angular_momentum.powi(2) * mu_recip) / (1.0 + eccentricity);
575
576        // Part 2: converting true anomaly to mean anomaly
577        // We first convert it to an eccentric anomaly:
578        let eccentric_anomaly = if eccentricity < 1.0 {
579            // let v = true_anomaly,
580            //   e = eccentricity,
581            //   E = eccentric anomaly
582            //
583            // https://en.wikipedia.org/wiki/True_anomaly#From_the_eccentric_anomaly:
584            // tan(v / 2) = sqrt((1 + e)/(1 - e)) * tan(E / 2)
585            // 1 = sqrt((1 + e)/(1 - e)) * tan(E / 2) / tan(v / 2)
586            // 1 / tan(E / 2) = sqrt((1 + e)/(1 - e)) / tan(v / 2)
587            // tan(E / 2) = tan(v / 2) / sqrt((1 + e)/(1 - e))
588            // E / 2 = atan(tan(v / 2) / sqrt((1 + e)/(1 - e)))
589            // E = 2 * atan(tan(v / 2) / sqrt((1 + e)/(1 - e)))
590            // E = 2 * atan(tan(v / 2) * sqrt((1 - e)/(1 + e)))
591
592            2.0 * ((true_anomaly * 0.5).tan()
593                * ((1.0 - eccentricity) / (1.0 + eccentricity)).sqrt())
594            .atan()
595        } else {
596            // From the presentation "Spacecraft Dynamics and Control"
597            // by Matthew M. Peet
598            // https://control.asu.edu/Classes/MAE462/462Lecture05.pdf
599            // Slide 25 of 27
600            // Section "The Method for Hyperbolic Orbits"
601            //
602            // tan(f/2) = sqrt((e+1)/(e-1))*tanh(H/2)
603            // 1 / tanh(H/2) = sqrt((e+1)/(e-1)) / tan(f/2)
604            // tanh(H/2) = tan(f/2) / sqrt((e+1)/(e-1))
605            // tanh(H/2) = tan(f/2) * sqrt((e-1)/(e+1))
606            // H/2 = atanh(tan(f/2) * sqrt((e-1)/(e+1)))
607            // H = 2 atanh(tan(f/2) * sqrt((e-1)/(e+1)))
608            2.0 * ((true_anomaly * 0.5).tan()
609                * ((eccentricity - 1.0) / (eccentricity + 1.0)).sqrt())
610            .atanh()
611        };
612
613        // Then use Kepler's Equation to convert eccentric anomaly
614        // to mean anomaly:
615        let mean_anomaly = if eccentricity < 1.0 {
616            // https://en.wikipedia.org/wiki/Kepler%27s_equation#Equation
617            //
618            //      M = E - e sin E
619            //
620            // where:
621            //   M = mean anomaly
622            //   E = eccentric anomaly
623            //   e = eccentricity
624            eccentric_anomaly - eccentricity * eccentric_anomaly.sin()
625        } else {
626            // https://en.wikipedia.org/wiki/Kepler%27s_equation#Hyperbolic_Kepler_equation
627            //
628            //      M = e sinh(H) - H
629            //
630            // where:
631            //   M = mean anomaly
632            //   e = eccentricity
633            //   H = hyperbolic eccentric anomaly
634            eccentricity * eccentric_anomaly.sinh() - eccentric_anomaly
635        };
636
637        // We now have the actual mean anomaly (`M`) at the current time (`t`)
638        // We want to get the mean anomaly *at epoch* (`M_0`)
639        // This means offsetting the mean anomaly using the given current timestamp (`t`)
640        //
641        // M = t * sqrt(mu / |a^3|) + M_0
642        // M - M_0 = t * sqrt(mu / |a^3|)
643        // -M_0 = t * sqrt(mu / |a^3|) - M
644        // M_0 = M - t * sqrt(mu / |a^3|)
645        //
646        // a = r_p / (1 - e)
647        let semi_major_axis: f64 = periapsis / (1.0 - eccentricity);
648        let offset = time * (mu / semi_major_axis.powi(3).abs()).sqrt();
649        let mean_anomaly_at_epoch = mean_anomaly - offset;
650        let mean_anomaly_at_epoch = if eccentricity < 1.0 {
651            mean_anomaly_at_epoch.rem_euclid(TAU)
652        } else {
653            mean_anomaly_at_epoch
654        };
655
656        CompactOrbit::new(
657            eccentricity,
658            periapsis,
659            inclination,
660            arg_pe,
661            long_asc_node,
662            mean_anomaly_at_epoch,
663            mu,
664        )
665    }
666
667    /// Create a new [`Orbit`] struct from the state
668    /// vectors and a given mu value.
669    ///
670    /// # Mu
671    /// Mu is also known as the gravitational parameter, and
672    /// is equal to `GM`, where `G` is the gravitational constant,
673    /// and `M` is the mass of the parent body.  
674    /// It can be described as how strongly the parent body pulls on
675    /// the orbiting body.
676    ///
677    /// Learn more about the gravitational parameter:
678    /// <https://en.wikipedia.org/wiki/Standard_gravitational_parameter>
679    ///
680    /// # Time
681    /// The time passed into the function is measured in seconds.
682    ///
683    /// # Performance
684    /// This function is not too performant as it uses several trigonometric operations.  
685    ///
686    /// For single conversions, this is slower than
687    /// [the compact orbit converter][Self::to_compact_orbit], as there are some extra
688    /// values that will be calculated and cached.  
689    /// However, if you're going to use this same orbit for many calculations, this should
690    /// be better off in the long run as the caching performance benefits should outgrow
691    /// the small initialization cost.
692    ///
693    /// # Reference Frame
694    /// This function expects a state vector where the position's origin (0.0, 0.0, 0.0)
695    /// is the center of the parent body.
696    ///
697    /// # Parabolic Support
698    /// This function does not yet support parabolic trajectories.
699    /// Non-finite values may be returned for such cases.
700    ///
701    /// # Constraints
702    /// The position must not be at the origin, and the velocity must not be at zero.  
703    /// If this constraint is breached, you may get invalid values such as infinities
704    /// or NaNs.
705    ///
706    /// # Examples
707    /// Simple use-case:
708    /// ```
709    /// use keplerian_sim::{Orbit, OrbitTrait};
710    ///
711    /// let orbit = Orbit::default();
712    /// let mu = orbit.get_gravitational_parameter();
713    /// let time = 0.0;
714    ///
715    /// let sv = orbit.get_state_vectors_at_time(time);
716    ///
717    /// let new_orbit = sv.to_cached_orbit(mu, time);
718    ///
719    /// assert_eq!(orbit.get_eccentricity(), new_orbit.get_eccentricity());
720    /// assert_eq!(orbit.get_periapsis(), new_orbit.get_periapsis());
721    /// ```
722    /// To simulate an instantaneous 0.1 m/s prograde burn at periapsis:
723    /// ```
724    /// use keplerian_sim::{Orbit, OrbitTrait, StateVectors};
725    /// use glam::DVec3;
726    ///
727    /// let orbit = Orbit::default();
728    /// let mu = orbit.get_gravitational_parameter();
729    /// let time = 0.0;
730    ///
731    /// let sv = orbit.get_state_vectors_at_time(time);
732    /// assert_eq!(
733    ///     sv,
734    ///     StateVectors {
735    ///         position: DVec3::new(1.0, 0.0, 0.0),
736    ///         velocity: DVec3::new(0.0, 1.0, 0.0),
737    ///     }
738    /// );
739    ///
740    /// let new_sv = StateVectors {
741    ///     velocity: sv.velocity + DVec3::new(0.0, 0.1, 0.0),
742    ///     ..sv
743    /// };
744    ///
745    /// let new_orbit = new_sv.to_cached_orbit(mu, time);
746    ///
747    /// assert_eq!(
748    ///     new_orbit,
749    ///     Orbit::new(
750    ///         0.2100000000000002, // eccentricity
751    ///         1.0, // periapsis
752    ///         0.0, // inclination
753    ///         0.0, // argument of periapsis
754    ///         0.0, // longitude of ascending node
755    ///         0.0, // mean anomaly
756    ///         1.0, // gravitational parameter
757    ///     )
758    /// )
759    /// ```
760    #[must_use]
761    pub fn to_cached_orbit(self, mu: f64, time: f64) -> Orbit {
762        self.to_compact_orbit(mu, time).into()
763    }
764
765    /// Create a new custom orbit struct from the state vectors
766    /// and a given mu value.
767    ///
768    /// # Mu
769    /// Mu is also known as the gravitational parameter, and
770    /// is equal to `GM`, where `G` is the gravitational constant,
771    /// and `M` is the mass of the parent body.  
772    /// It can be described as how strongly the parent body pulls on
773    /// the orbiting body.
774    ///
775    /// Learn more about the gravitational parameter:
776    /// <https://en.wikipedia.org/wiki/Standard_gravitational_parameter>
777    ///
778    /// # Time
779    /// The time passed into the function is measured in seconds.
780    ///
781    /// # Performance
782    /// This function is not too performant as it uses several trigonometric operations.
783    ///
784    /// The performance also depends on how fast the specified orbit type can convert
785    /// between the [`CompactOrbit`] form into itself, and so we cannot guarantee any
786    /// performance behaviors.
787    ///
788    /// # Reference Frame
789    /// This function expects a state vector where the position's origin (0.0, 0.0, 0.0)
790    /// is the center of the parent body.
791    ///
792    /// # Parabolic Support
793    /// This function does not yet support parabolic trajectories.
794    /// Non-finite values may be returned for such cases.
795    ///
796    /// # Constraints
797    /// The position must not be at the origin, and the velocity must not be at zero.  
798    /// If this constraint is breached, you may get invalid values such as infinities
799    /// or NaNs.
800    #[must_use]
801    pub fn to_custom_orbit<O>(self, mu: f64, time: f64) -> O
802    where
803        O: From<CompactOrbit> + OrbitTrait,
804    {
805        self.to_compact_orbit(mu, time).into()
806    }
807}
808
809/// A trait that defines the methods that a Keplerian orbit must implement.
810///
811/// This trait is implemented by both [`Orbit`] and [`CompactOrbit`].
812///
813/// # Examples
814/// ```
815/// use keplerian_sim::{Orbit, OrbitTrait, CompactOrbit};
816///
817/// fn accepts_orbit(orbit: &impl OrbitTrait) {
818///     println!("That's an orbit!");
819/// }
820///
821/// fn main() {
822///     let orbit = Orbit::default();
823///     accepts_orbit(&orbit);
824///
825///     let compact = CompactOrbit::default();
826///     accepts_orbit(&compact);
827/// }
828/// ```
829///
830/// This example will fail to compile:
831///
832/// ```compile_fail
833/// # use keplerian_sim::{Orbit, OrbitTrait, CompactOrbit};
834/// #
835/// # fn accepts_orbit(orbit: &impl OrbitTrait) {
836/// #     println!("That's an orbit!");
837/// # }
838/// #
839/// # fn main() {
840/// #     let orbit = Orbit::default();
841/// #     accepts_orbit(&orbit);
842/// #  
843/// #     let compact = CompactOrbit::default();
844/// #     accepts_orbit(&compact);
845///       let not_orbit = (0.0, 1.0);
846///       accepts_orbit(&not_orbit);
847/// # }
848/// ```
849pub trait OrbitTrait {
850    /// Gets the semi-major axis of the orbit.
851    ///
852    /// In an elliptic orbit, the semi-major axis is the
853    /// average of the apoapsis and periapsis.  
854    /// This function uses a generalization which uses
855    /// eccentricity instead.
856    ///
857    /// This function returns infinity for parabolic orbits,
858    /// and negative values for hyperbolic orbits.
859    ///
860    /// Learn more: <https://en.wikipedia.org/wiki/Semi-major_and_semi-minor_axes>
861    ///
862    /// # Example
863    /// ```
864    /// use keplerian_sim::{Orbit, OrbitTrait};
865    ///
866    /// let mut orbit = Orbit::default();
867    /// orbit.set_periapsis(50.0);
868    /// orbit.set_apoapsis_force(100.0);
869    /// let sma = orbit.get_semi_major_axis();
870    /// let expected = 75.0;
871    /// assert!((sma - expected).abs() < 1e-6);
872    /// ```
873    ///
874    /// # Performance
875    /// This function is very performant and should not be the cause of any
876    /// performance issues.
877    fn get_semi_major_axis(&self) -> f64 {
878        // a = r_p / (1 - e)
879        self.get_periapsis() / (1.0 - self.get_eccentricity())
880    }
881
882    /// Gets the semi-minor axis of the orbit.
883    ///
884    /// In an elliptic orbit, the semi-minor axis is half of the maximum "width"
885    /// of the orbit.
886    ///
887    /// Learn more: <https://en.wikipedia.org/wiki/Semi-major_and_semi-minor_axes>
888    ///
889    /// # Performance
890    /// This function is performant and is unlikely to be the cause of any
891    /// performance issues.
892    fn get_semi_minor_axis(&self) -> f64 {
893        self.get_semi_major_axis() * (1.0 - self.get_eccentricity().powi(2)).abs().sqrt()
894    }
895
896    /// Gets the semi-latus rectum of the orbit, in meters.
897    ///
898    /// Learn more: <https://en.wikipedia.org/wiki/Ellipse#Semi-latus_rectum>  
899    /// <https://en.wikipedia.org/wiki/Conic_section#Conic_parameters>
900    ///
901    /// # Performance
902    /// This function is very performant and should not be the cause of any
903    /// performance issues.
904    fn get_semi_latus_rectum(&self) -> f64 {
905        // https://control.asu.edu/Classes/MAE462/462Lecture03.pdf
906        // Lecture on Spacecraft Dynamics and Control
907        // by Matthew M. Peet
908        // Slide 20 (or 12?), section titled "Periapse for all Orbits"
909        // r_p = p / (1 + e)
910        // ...where r_p = periapsis; p = semi-latus rectum; e = eccentricity.
911        // => r_p (1 + e) = p
912        self.get_periapsis() * (1.0 + self.get_eccentricity())
913    }
914
915    /// Gets the linear eccentricity of the orbit, in meters.
916    ///
917    /// In an elliptic orbit, the linear eccentricity is the distance
918    /// between its center and either of its two foci (focuses).
919    ///
920    /// # Performance
921    /// This function is very performant and should not be the cause of any
922    /// performance issues.
923    ///
924    /// # Example
925    /// ```
926    /// use keplerian_sim::{Orbit, OrbitTrait};
927    ///
928    /// let mut orbit = Orbit::default();
929    /// orbit.set_periapsis(50.0);
930    /// orbit.set_apoapsis_force(100.0);
931    ///
932    /// // Let's say the periapsis is at x = -50.
933    /// // The apoapsis would be at x = 100.
934    /// // The midpoint would be at x = 25.
935    /// // The parent body - one of its foci - is always at the origin (x = 0).
936    /// // This means the linear eccentricity is 25.
937    ///
938    /// let linear_eccentricity = orbit.get_linear_eccentricity();
939    /// let expected = 25.0;
940    ///
941    /// assert!((linear_eccentricity - expected).abs() < 1e-6);
942    /// ```
943    fn get_linear_eccentricity(&self) -> f64 {
944        self.get_semi_major_axis() - self.get_periapsis()
945    }
946
947    /// Gets the true anomaly asymptote (f_∞) of the hyperbolic trajectory.
948    ///
949    /// This returns a positive number between π/2 and π for open
950    /// trajectories, and NaN for closed orbits.
951    ///
952    /// This can be used to get the range of possible true anomalies that
953    /// a hyperbolic trajectory can be in.  
954    /// This function returns the maximum true anomaly, and the minimum
955    /// true anomaly can be derived simply by negating the result:
956    /// ```text
957    /// f_-∞ = -f_∞
958    /// ```
959    /// The minimum and maximum together represent the range of possible
960    /// true anomalies.
961    ///
962    /// # Performance
963    /// This function is moderately performant and contains only one
964    /// trigonometry operation.
965    ///
966    /// # Example
967    /// ```
968    /// use keplerian_sim::{Orbit, OrbitTrait};
969    ///
970    /// // Closed (elliptic) orbit with eccentricity = 0.8
971    /// let closed = Orbit::new_flat(0.8, 1.0, 0.0, 0.0, 1.0);
972    ///
973    /// // True anomaly asymptote is only defined for open orbits,
974    /// // i.e., eccentricity ≥ 1
975    /// assert!(closed.get_true_anomaly_at_asymptote().is_nan());
976    ///
977    /// let parabolic = Orbit::new_flat(1.0, 1.0, 0.0, 0.0, 1.0);
978    /// assert_eq!(
979    ///     parabolic.get_true_anomaly_at_asymptote(),
980    ///     std::f64::consts::PI
981    /// );
982    ///
983    /// let hyperbolic = Orbit::new_flat(2.0, 1.0, 0.0, 0.0, 1.0);
984    /// let asymptote = 2.0943951023931957;
985    /// assert_eq!(
986    ///     hyperbolic.get_true_anomaly_at_asymptote(),
987    ///     asymptote
988    /// );
989    ///
990    /// // At the asymptote, the altitude is infinite.
991    /// // Note: We can't use the regular `get_altitude_at_true_anomaly` here
992    /// // because it is less accurate (since it uses cos() while the asymptote uses
993    /// // acos(), and the roundtrip causes precision loss).
994    /// // We use the unchecked version with the exact cosine value
995    /// // of the true anomaly (-1/e) to avoid float inaccuracies.
996    /// let asymptote_cos = -1.0 / hyperbolic.get_eccentricity();
997    ///
998    /// // We first check that asymptote_cos is close to cos(asymptote):
999    /// assert!(
1000    ///     (asymptote_cos - asymptote.cos()).abs() < 1e-15
1001    /// );
1002    ///
1003    /// // Then we can be fairly confident this will be exactly infinite:
1004    /// assert!(
1005    ///     hyperbolic
1006    ///         .get_altitude_at_true_anomaly_unchecked(
1007    ///             hyperbolic.get_semi_latus_rectum(),
1008    ///             asymptote_cos
1009    ///         )
1010    ///         .is_infinite()
1011    /// )
1012    /// ```
1013    #[doc(alias = "get_theta_infinity")]
1014    #[doc(alias = "get_hyperbolic_true_anomaly_range")]
1015    fn get_true_anomaly_at_asymptote(&self) -> f64 {
1016        // https://en.wikipedia.org/wiki/Hyperbolic_trajectory#Parameters_describing_a_hyperbolic_trajectory
1017        // 2f_∞ = 2cos^-1(-1/e)
1018        // ⇒ f_∞ = acos(-1/e)
1019        use std::ops::Neg;
1020        self.get_eccentricity().recip().neg().acos()
1021    }
1022
1023    /// Gets the position of the orbit at periapsis.
1024    ///
1025    /// # Performance
1026    /// In the cached orbit struct ([`Orbit`]), this function is
1027    /// very performant and only involves three multiplications.
1028    ///
1029    /// However, in the compact orbit struct ([`CompactOrbit`]), this
1030    /// is a lot slower and involves some trigonometric calculations.
1031    /// If you already know the P basis vector of the PQW coordinate system,
1032    /// you may use the unchecked version instead
1033    /// ([`get_position_at_periapsis_unchecked`][OrbitTrait::get_position_at_periapsis_unchecked]),
1034    /// which is a lot faster and would skip repeated calculations.
1035    ///
1036    /// # Example
1037    /// ```
1038    /// use keplerian_sim::{Orbit, OrbitTrait};
1039    /// use glam::DVec3;
1040    ///
1041    /// let orbit = Orbit::new_flat(
1042    ///     0.25, // Eccentricity
1043    ///     1.0, // Periapsis
1044    ///     0.0, // Argument of periapsis
1045    ///     0.0, // Mean anomaly at epoch
1046    ///     1.0, // Gravitational parameter
1047    /// );
1048    ///
1049    /// assert_eq!(
1050    ///     orbit.get_position_at_periapsis(),
1051    ///     DVec3::new(1.0, 0.0, 0.0)
1052    /// );
1053    /// ```
1054    #[inline]
1055    fn get_position_at_periapsis(&self) -> DVec3 {
1056        self.get_position_at_periapsis_unchecked(self.get_pqw_basis_vector_p())
1057    }
1058
1059    /// Gets the position of the orbit at periapsis
1060    /// based on a known P basis vector.
1061    ///
1062    /// The P basis vector is one of the basis vector from the PQW
1063    /// coordinate system.
1064    ///
1065    /// Learn more about the PQW system: <https://en.wikipedia.org/wiki/Perifocal_coordinate_system>
1066    ///
1067    /// # Unchecked Operation
1068    /// This function does not check that the given `p_vector`
1069    /// is a unit vector nor whether it actually is the basis vector in the
1070    /// PQW coordinate system.
1071    ///
1072    /// It is expected that callers get this vector from either
1073    /// the transformation matrix
1074    /// ([`get_transformation_matrix`][OrbitTrait::get_transformation_matrix]),
1075    /// the basis vector collective getter
1076    /// ([`get_pqw_basis_vectors`][OrbitTrait::get_pqw_basis_vectors]),
1077    /// or the individual basis vector getter
1078    /// ([`get_pqw_basis_vector_p`][OrbitTrait::get_pqw_basis_vector_p]).
1079    ///
1080    /// A safe wrapper is available, but that may be slower; see the
1081    /// Performance section for details.
1082    ///
1083    /// # Performance
1084    /// There is no reason to use this if you are using the cached
1085    /// orbit struct ([`Orbit`]) as the performance is identical to the
1086    /// wrapper function.
1087    ///
1088    /// However, in the compact orbit struct ([`CompactOrbit`]), this
1089    /// skips some expensive trigonometry operations and therefore is
1090    /// a lot faster than the wrapper function.
1091    ///
1092    /// # Example
1093    /// ```
1094    /// use keplerian_sim::{CompactOrbit, OrbitTrait};
1095    /// use glam::DVec3;
1096    ///
1097    /// let orbit = CompactOrbit::new_flat(
1098    ///     0.25, // Eccentricity
1099    ///     1.0, // Periapsis
1100    ///     0.0, // Argument of periapsis
1101    ///     0.0, // Mean anomaly at epoch
1102    ///     1.0, // Gravitational parameter
1103    /// );
1104    ///
1105    /// let p_vector = orbit.get_pqw_basis_vector_p();
1106    ///
1107    /// // Use p_vector here...
1108    /// // Use it again for periapsis position!
1109    ///
1110    /// assert_eq!(
1111    ///     orbit.get_position_at_periapsis_unchecked(p_vector),
1112    ///     DVec3::new(1.0, 0.0, 0.0)
1113    /// );
1114    /// ```
1115    #[inline]
1116    fn get_position_at_periapsis_unchecked(&self, p_vector: DVec3) -> DVec3 {
1117        self.get_periapsis() * p_vector
1118    }
1119
1120    /// Gets the position of the orbit at apoapsis.
1121    ///
1122    /// # Performance
1123    /// In the cached orbit struct ([`Orbit`]), this function is
1124    /// very performant and only involves three multiplications.
1125    ///
1126    /// However, in the compact orbit struct ([`CompactOrbit`]), this
1127    /// is a lot slower and involves some trigonometric calculations.
1128    /// If you already know the P basis vector of the PQW coordinate system,
1129    /// you may use the unchecked version instead
1130    /// ([`get_position_at_apoapsis_unchecked`][OrbitTrait::get_position_at_apoapsis_unchecked]),
1131    /// which is a lot faster and would skip repeated calculations.
1132    ///
1133    /// # Example
1134    /// ```
1135    /// use keplerian_sim::{Orbit, OrbitTrait};
1136    /// use glam::DVec3;
1137    ///
1138    /// let orbit = Orbit::new_flat(
1139    ///     0.25, // Eccentricity
1140    ///     1.0, // Periapsis
1141    ///     0.0, // Argument of periapsis
1142    ///     0.0, // Mean anomaly at epoch
1143    ///     1.0, // Gravitational parameter
1144    /// );
1145    ///
1146    /// assert_eq!(
1147    ///     orbit.get_position_at_apoapsis(),
1148    ///     DVec3::new(-1.6666666666666665, 0.0, 0.0)
1149    /// );
1150    /// ```
1151    #[inline]
1152    fn get_position_at_apoapsis(&self) -> DVec3 {
1153        self.get_position_at_apoapsis_unchecked(self.get_pqw_basis_vector_p())
1154    }
1155
1156    /// Gets the position of the orbit at apoapsis
1157    /// based on a known P basis vector.
1158    ///
1159    /// The P basis vector is one of the basis vector from the PQW
1160    /// coordinate system.
1161    ///
1162    /// Learn more about the PQW system: <https://en.wikipedia.org/wiki/Perifocal_coordinate_system>
1163    ///
1164    /// # Unchecked Operation
1165    /// This function does not check that the given `p_vector`
1166    /// is a unit vector nor whether it actually is the basis vector in the
1167    /// PQW coordinate system.
1168    ///
1169    /// It is expected that callers get this vector from either
1170    /// the transformation matrix
1171    /// ([`get_transformation_matrix`][OrbitTrait::get_transformation_matrix]),
1172    /// the basis vector collective getter
1173    /// ([`get_pqw_basis_vectors`][OrbitTrait::get_pqw_basis_vectors]),
1174    /// or the individual basis vector getter
1175    /// ([`get_pqw_basis_vector_p`][OrbitTrait::get_pqw_basis_vector_p]).
1176    ///
1177    /// A safe wrapper is available, but that may be slower; see the
1178    /// Performance section for details.
1179    ///
1180    /// # Performance
1181    /// There is no reason to use this if you are using the cached
1182    /// orbit struct ([`Orbit`]) as the performance is identical to the
1183    /// wrapper function.
1184    ///
1185    /// However, in the compact orbit struct ([`CompactOrbit`]), this
1186    /// skips some expensive trigonometry operations and therefore is
1187    /// a lot faster than the wrapper function.
1188    ///
1189    /// # Example
1190    /// ```
1191    /// use keplerian_sim::{CompactOrbit, OrbitTrait};
1192    /// use glam::DVec3;
1193    ///
1194    /// let orbit = CompactOrbit::new_flat(
1195    ///     0.25, // Eccentricity
1196    ///     1.0, // Periapsis
1197    ///     0.0, // Argument of periapsis
1198    ///     0.0, // Mean anomaly at epoch
1199    ///     1.0, // Gravitational parameter
1200    /// );
1201    ///
1202    /// let p_vector = orbit.get_pqw_basis_vector_p();
1203    ///
1204    /// // Use p_vector here...
1205    /// // Use it again for apoapsis position!
1206    ///
1207    /// assert_eq!(
1208    ///     orbit.get_position_at_apoapsis_unchecked(p_vector),
1209    ///     DVec3::new(-1.6666666666666665, 0.0, 0.0)
1210    /// );
1211    /// ```
1212    #[inline]
1213    fn get_position_at_apoapsis_unchecked(&self, p_vector: DVec3) -> DVec3 {
1214        -self.get_apoapsis() * p_vector
1215    }
1216
1217    /// Gets the apoapsis of the orbit.  
1218    /// Returns infinity for parabolic orbits.  
1219    /// Returns negative values for hyperbolic orbits.  
1220    ///
1221    /// # Performance
1222    /// This function is very performant and should not be the cause of any
1223    /// performance issues.
1224    ///
1225    /// # Examples
1226    /// ```
1227    /// use keplerian_sim::{Orbit, OrbitTrait};
1228    ///
1229    /// let mut orbit = Orbit::default();
1230    /// orbit.set_eccentricity(0.5); // Elliptic
1231    /// assert!(orbit.get_apoapsis() > 0.0);
1232    ///
1233    /// orbit.set_eccentricity(1.0); // Parabolic
1234    /// assert!(orbit.get_apoapsis().is_infinite());
1235    ///
1236    /// orbit.set_eccentricity(2.0); // Hyperbolic
1237    /// assert!(orbit.get_apoapsis() < 0.0);
1238    /// ```
1239    fn get_apoapsis(&self) -> f64 {
1240        let eccentricity = self.get_eccentricity();
1241        if eccentricity == 1.0 {
1242            f64::INFINITY
1243        } else {
1244            self.get_semi_major_axis() * (1.0 + eccentricity)
1245        }
1246    }
1247
1248    /// Sets the apoapsis of the orbit.  
1249    /// Errors when the apoapsis is less than the periapsis, or less than zero.  
1250    /// If you want a setter that does not error, use `set_apoapsis_force`, which will
1251    /// try its best to interpret what you might have meant, but may have
1252    /// undesirable behavior.
1253    ///
1254    /// # Examples
1255    /// ```
1256    /// use keplerian_sim::{Orbit, OrbitTrait};
1257    ///
1258    /// let mut orbit = Orbit::default();
1259    /// orbit.set_periapsis(50.0);
1260    ///
1261    /// assert!(
1262    ///     orbit.set_apoapsis(100.0)
1263    ///         .is_ok()
1264    /// );
1265    ///
1266    /// let result = orbit.set_apoapsis(25.0);
1267    /// assert!(result.is_err());
1268    /// assert_eq!(
1269    ///     result.unwrap_err(),
1270    ///     keplerian_sim::ApoapsisSetterError::ApoapsisLessThanPeriapsis
1271    /// );
1272    ///
1273    /// let result = orbit.set_apoapsis(-25.0);
1274    /// assert!(result.is_err());
1275    /// assert_eq!(
1276    ///     result.unwrap_err(),
1277    ///     keplerian_sim::ApoapsisSetterError::ApoapsisNegative
1278    /// );
1279    /// ```
1280    fn set_apoapsis(&mut self, apoapsis: f64) -> Result<(), ApoapsisSetterError>;
1281
1282    /// Sets the apoapsis of the orbit, with a best-effort attempt at interpreting
1283    /// possibly-invalid values.  
1284    /// This function will not error, but may have undesirable behavior:
1285    /// - If the given apoapsis is less than the periapsis but more than zero,
1286    ///   the orbit will be flipped and the periapsis will be set to the given apoapsis.
1287    /// - If the given apoapsis is negative but between zero and negative periapsis,
1288    ///   the apoapsis will get treated as infinity and the orbit will be parabolic.
1289    ///   (This is because even in hyperbolic orbits, apoapsis cannot be between 0 and -periapsis)  
1290    /// - If the given apoapsis is negative AND less than negative periapsis,
1291    ///   the orbit will be hyperbolic.
1292    ///
1293    /// If these behaviors are undesirable, consider creating a custom wrapper around
1294    /// `set_eccentricity` instead.
1295    ///
1296    /// # Examples
1297    /// ```
1298    /// use keplerian_sim::{Orbit, OrbitTrait};
1299    ///
1300    /// let mut base = Orbit::default();
1301    /// base.set_periapsis(50.0);
1302    ///
1303    /// let mut normal = base.clone();
1304    /// // Set the apoapsis to 100
1305    /// normal.set_apoapsis_force(100.0);
1306    /// assert_eq!(normal.get_apoapsis(), 99.99999999999997);
1307    /// assert_eq!(normal.get_periapsis(), 50.0);
1308    /// assert_eq!(normal.get_arg_pe(), 0.0);
1309    /// assert_eq!(normal.get_mean_anomaly_at_epoch(), 0.0);
1310    ///
1311    /// let mut flipped = base.clone();
1312    /// // Set the "apoapsis" to 25
1313    /// // This will flip the orbit, setting the altitude
1314    /// // where the current apoapsis is, to 25, and
1315    /// // flipping the orbit.
1316    /// // This sets the periapsis to 25, and the apoapsis to the
1317    /// // previous periapsis.
1318    /// flipped.set_apoapsis_force(25.0);
1319    /// assert_eq!(flipped.get_apoapsis(), 49.999999999999986);
1320    /// assert_eq!(flipped.get_periapsis(), 25.0);
1321    /// assert_eq!(flipped.get_arg_pe(), std::f64::consts::PI);
1322    /// assert_eq!(flipped.get_mean_anomaly_at_epoch(), std::f64::consts::PI);
1323    ///
1324    /// let mut hyperbolic = base.clone();
1325    /// // Set the "apoapsis" to -250
1326    /// hyperbolic.set_apoapsis_force(-250.0);
1327    /// assert_eq!(hyperbolic.get_apoapsis(), -250.0);
1328    /// assert_eq!(hyperbolic.get_periapsis(), 50.0);
1329    /// assert_eq!(hyperbolic.get_arg_pe(), 0.0);
1330    /// assert!(hyperbolic.get_eccentricity() > 1.0);
1331    /// assert_eq!(hyperbolic.get_mean_anomaly_at_epoch(), 0.0);
1332    ///
1333    /// let mut parabolic = base.clone();
1334    /// // Set the "apoapsis" to between 0 and -50
1335    /// // This will set the apoapsis to infinity, and the orbit will be parabolic.
1336    /// parabolic.set_apoapsis_force(-25.0);
1337    /// assert!(parabolic.get_apoapsis().is_infinite());
1338    /// assert_eq!(parabolic.get_periapsis(), 50.0);
1339    /// assert_eq!(parabolic.get_arg_pe(), 0.0);
1340    /// assert_eq!(parabolic.get_eccentricity(), 1.0);
1341    /// assert_eq!(parabolic.get_mean_anomaly_at_epoch(), 0.0);
1342    /// ```
1343    fn set_apoapsis_force(&mut self, apoapsis: f64);
1344
1345    /// Gets the mean motion of the orbit, in radians per second.
1346    ///
1347    /// Mean motion (represented by `n`) is the angular speed required for
1348    /// a body to complete one orbit, assuming constant speed in a circular
1349    /// orbit which completes in the same time as the variable speed,
1350    /// elliptical orbit of the actual body.
1351    ///
1352    /// \- [Wikipedia](https://en.wikipedia.org/wiki/Mean_motion)
1353    ///
1354    /// # Performance
1355    /// This function is performant and is unlikely to be the cause
1356    /// of any performance issues.
1357    ///
1358    /// # Example
1359    /// ```
1360    /// use keplerian_sim::{Orbit, OrbitTrait};
1361    ///
1362    /// let orbit = Orbit::new_flat(
1363    ///     0.8, // Eccentricity
1364    ///     9.4, // Periapsis
1365    ///     2.0, // Argument of periapsis
1366    ///     1.5, // Mean anomaly at epoch
1367    ///     0.8, // Gravitational parameter
1368    /// );
1369    ///
1370    /// let orbital_period = orbit.get_orbital_period();
1371    /// let mean_motion = std::f64::consts::TAU / orbital_period;
1372    ///
1373    /// assert!((orbit.get_mean_motion() - mean_motion).abs() < f64::EPSILON);
1374    /// ```
1375    fn get_mean_motion(&self) -> f64 {
1376        // n = TAU / T, radians
1377        //
1378        // note: T = 2pi * sqrt(a^3 / GM)
1379        //
1380        // => n = TAU / (TAU * sqrt(a^3 / GM))
1381        // => n = 1 / sqrt(a^3 / GM)
1382        // => n = sqrt(GM / a^3)
1383        (self.get_gravitational_parameter() / self.get_semi_major_axis().powi(3)).sqrt()
1384    }
1385
1386    /// Gets the focal parameter of the orbit, in meters.
1387    ///
1388    /// This returns infinity in circular orbits (e = 0).
1389    ///
1390    /// The focal parameter (p) is the distance from a focus
1391    /// to the corresponding directrix.
1392    ///
1393    /// \- [Wikipedia](https://en.wikipedia.org/wiki/Conic_section#Conic_parameters)
1394    ///
1395    /// # Performance
1396    /// This function is very performant and should not be
1397    /// the cause of any performance issues.
1398    ///
1399    /// # Example
1400    /// ```
1401    /// use keplerian_sim::{Orbit, OrbitTrait};
1402    ///
1403    /// let orbit = Orbit::new_flat(
1404    ///     1.0, // Eccentricity
1405    ///     3.0, // Periapsis
1406    ///     2.9, // Argument of periapsis
1407    ///     0.8, // Mean anomaly at epoch
1408    ///     1.9, // Gravitational parameter
1409    /// );
1410    ///
1411    /// // From Wikipedia's focal parameter equation for parabolas (e = 1)
1412    /// // (a in this case is periapsis distance, not semi-major axis)
1413    /// let expected_focal_parameter = 2.0 * orbit.get_periapsis();
1414    ///
1415    /// assert!(
1416    ///     (orbit.get_focal_parameter() - expected_focal_parameter).abs() < f64::EPSILON
1417    /// );
1418    /// ```
1419    fn get_focal_parameter(&self) -> f64 {
1420        // https://web.ma.utexas.edu/users/m408s/m408d/CurrentWeb/LM10-6-3.php
1421        //
1422        // > Polar equations of conic sections: If the directrix is a distance d away,
1423        // > then the polar form of a conic section with eccentricity e is
1424        // >
1425        // > r(θ) = ed / (1 - e cos (θ - θ_0))
1426        // >
1427        // > where the constant θ_0 depends on the direction of the directrix.
1428        // >
1429        // > If the directrix is the line x = d, then we have
1430        // > r = ed / (1 + e cos θ)
1431        //
1432        // Using periapsis (r = r_p, θ = ν = 0):
1433        // r_p = ed / (1 + e cos 0)
1434        // r_p = ed / (1 + e), since cos 0 = 1
1435        // 1 = ed / (r_p * (1 + e))
1436        // 1/d = e / (r_p * (1 + e))
1437        // d = r_p * (1 + e) / e
1438        self.get_periapsis() * (1.0 + self.get_eccentricity()) / self.get_eccentricity()
1439    }
1440
1441    /// Gets the specific angular momentum of the orbit, in
1442    /// square meters per second (m^2/s).
1443    ///
1444    /// The specific relative angular momentum of a body is the
1445    /// angular momentum of that body divided by its mass.
1446    ///
1447    /// \- [Wikipedia](https://en.wikipedia.org/wiki/Specific_angular_momentum)
1448    ///
1449    /// # Performance
1450    /// This function is performant and is unlikely to be the cause
1451    /// of any performance issues.
1452    ///
1453    /// # Example
1454    /// ```
1455    /// use keplerian_sim::{Orbit, OrbitTrait};
1456    ///
1457    /// let orbit = Orbit::new_flat(
1458    ///     1.2, // Eccentricity
1459    ///     2.0, // Periapsis
1460    ///     3.0, // Argument of periapsis
1461    ///     4.8, // Mean anomaly at epoch
1462    ///     5.0, // Gravitational parameter
1463    /// );
1464    ///
1465    /// const EXPECTED_VALUE: f64 = 4.69041575982343;
1466    ///
1467    /// let momentum =
1468    ///     orbit.get_specific_angular_momentum();
1469    ///
1470    /// assert!((momentum - EXPECTED_VALUE).abs() < f64::EPSILON);
1471    /// ```
1472    fn get_specific_angular_momentum(&self) -> f64 {
1473        // https://faculty.fiu.edu/~vanhamme/ast3213/orbits.pdf
1474        // Page 3, eq. 17: "h = sqrt(μa(1 - e^2))"
1475        //
1476        // to simplify:
1477        // inner := a(1 - e^2)
1478        // => h = sqrt(μ * inner)
1479        //
1480        // recall a = r_p / (1 - e)
1481        //
1482        // => inner = (r_p / (1 - e))(1 - e^2)
1483        // => inner = (r_p / (1 - e))(1 - e)(1 + e)
1484        // => inner = r_p (1 + e)
1485        // => inner = semi-latus rectum `p`
1486        // (see [OrbitTrait::get_semi_latus_rectum])
1487        // => h = sqrt(μp)
1488        (self.get_gravitational_parameter() * self.get_semi_latus_rectum()).sqrt()
1489    }
1490
1491    /// Gets the specific orbital energy `ε` of the orbit,
1492    /// in joules per kilogram (J/kg, equiv. to m^2 ⋅ s^-2).
1493    ///
1494    /// For closed orbits (eccentricity < 0), ε < 0.  
1495    /// When eccentricity equals 1 (parabolic), ε equals 0,
1496    /// and when eccentricity exceeds 1 (hyperbolic), ε is positive.
1497    ///
1498    /// The specific orbital energy ε of two orbiting bodies is
1499    /// the constant quotient of their mechanical energy
1500    /// (the sum of their mutual potential energy, ε_p, and their
1501    /// kinetic energy, ε_k) to their reduced mass.
1502    ///
1503    /// \- [Wikipedia](https://en.wikipedia.org/wiki/Specific_orbital_energy)
1504    ///
1505    /// # Performance
1506    /// This function is very performant and should not be the
1507    /// cause of any performance issues.
1508    ///
1509    /// # Example
1510    /// ```
1511    /// use keplerian_sim::{Orbit, OrbitTrait};
1512    ///
1513    /// let elliptic = Orbit::new_flat(0.3, 1.0, 0.0, 0.0, 1.0);
1514    /// let parabolic = Orbit::new_flat(1.0, 1.0, 0.0, 0.0, 1.0);
1515    /// let hyperbolic = Orbit::new_flat(2.6, 1.0, 0.0, 0.0, 1.0);
1516    ///
1517    /// assert!(elliptic.get_specific_orbital_energy() < 0.0);
1518    /// assert!(parabolic.get_specific_orbital_energy() == 0.0);
1519    /// assert!(hyperbolic.get_specific_orbital_energy() > 0.0);
1520    /// ```
1521    fn get_specific_orbital_energy(&self) -> f64 {
1522        // https://en.wikipedia.org/wiki/Specific_orbital_energy
1523        // ε = -μ / (2a)
1524        //
1525        // note: a = r_p / (1 - e)
1526        // => 1 / a = (1 - e) / r_p
1527        //
1528        // => ε = -μ (1 - e) / r_p
1529        -self.get_gravitational_parameter() * (1.0 - self.get_eccentricity()) / self.get_periapsis()
1530    }
1531
1532    /// Gets the area swept out by the orbit in square meters
1533    /// per second (m^2/s).
1534    ///
1535    /// # Performance
1536    /// This function is very performant and should not
1537    /// be the cause of any performance issues.
1538    ///
1539    /// # Example
1540    /// ```
1541    /// use keplerian_sim::{Orbit, OrbitTrait};
1542    ///
1543    /// let orbit = Orbit::new_flat(
1544    ///     2.9, // Eccentricity
1545    ///     4.9, // Periapsis
1546    ///     0.2, // Argument of periapsis
1547    ///     0.9, // Mean anomaly at epoch
1548    ///     9.8, // Gravitational parameter
1549    /// );
1550    ///
1551    /// const EXPECTED_RATE: f64 = 6.842477621446782;
1552    ///
1553    /// assert!(
1554    ///     (orbit.get_area_sweep_rate() - EXPECTED_RATE).abs() < f64::EPSILON
1555    /// );
1556    /// ```
1557    fn get_area_sweep_rate(&self) -> f64 {
1558        // We can measure the instantaneous area sweep rate
1559        // at periapsis.
1560        // As the delta-time gets smaller and smaller,
1561        // a triangle approximation gets more and more accurate.
1562        // (This is basically a derivative in calculus)
1563        // That approximation triangle has a length equal to
1564        // the speed at periapsis, and a height equal to the
1565        // radius/altitude at periapsis.
1566        // This means the triangle has an area of (1/2 * base * height)
1567        // = 1/2 * speed_at_periapsis * periapsis
1568        //
1569        // This would be the area sweep rate at the periapsis
1570        // of the orbit. We can then utilize Kepler's 2nd law:
1571        //
1572        //     A line segment joining a planet and the Sun
1573        //     sweeps out equal areas during equal intervals of time.
1574        //
1575        // (from https://en.wikipedia.org/wiki/Kepler%27s_laws_of_planetary_motion)
1576        //
1577        // This means this actually is a constant throughout
1578        // the orbit, so the equation below is valid.
1579
1580        0.5 * self.get_speed_at_periapsis() * self.get_periapsis()
1581    }
1582
1583    // TODO: PARABOLIC SUPPORT: This function returns NaN on parabolic
1584    /// Gets the time when the orbit is in periapsis, in seconds since epoch.
1585    ///
1586    /// This returns the time when mean anomaly equals zero.  
1587    /// This means although it will represent a time of periapsis,
1588    /// it doesn't mean "next periapsis" nor "previous periapsis",
1589    /// it just means "a periapsis", at least for closed orbits
1590    /// (e < 1).
1591    ///
1592    /// # Parabolic Support
1593    /// This function does not support parabolic trajectories yet.  
1594    /// Calling this function on a parabolic trajectory results in a
1595    /// non-finite number.
1596    ///
1597    /// # Performance
1598    /// This function is performant and is unlikely to be the cause
1599    /// of any performance issues.
1600    ///
1601    /// # Example
1602    /// ```
1603    /// use keplerian_sim::{Orbit, OrbitTrait};
1604    ///
1605    /// const PERIAPSIS: f64 = 1.0;
1606    ///
1607    /// let orbit = Orbit::new_flat(
1608    ///     0.3, // Eccentricity
1609    ///     PERIAPSIS,
1610    ///     2.9, // Argument of periapsis
1611    ///     1.5, // Mean anomaly at epoch
1612    ///     1.0, // Gravitational parameter
1613    /// );
1614    ///
1615    /// let time_of_pe = orbit.get_time_of_periapsis();
1616    ///
1617    /// let alt_of_pe = orbit.get_altitude_at_time(time_of_pe);
1618    ///
1619    /// assert!(
1620    ///     (alt_of_pe - PERIAPSIS).abs() < 1e-15
1621    /// );
1622    /// ```
1623    fn get_time_of_periapsis(&self) -> f64 {
1624        // We want to find M = 0
1625        // Per `get_mean_anomaly_at_time`:
1626        // M = t * sqrt(mu / |a^3|) + M_0
1627        // => 0 = t * sqrt(mu / |a^3|) + M_0
1628        // => -M_0 = t * sqrt(mu / |a^3|)
1629        // => t * sqrt(mu / |a^3|) = -M_0
1630        // => t = -M_0 / sqrt(mu / |a^3|)
1631        //
1632        // note: 1 / sqrt(mu / |a^3|) = sqrt(|a^3| / mu)
1633        //
1634        // => t = -M_0 * sqrt(|a^3| / mu)
1635
1636        -self.get_mean_anomaly_at_epoch()
1637            * (self.get_semi_major_axis().powi(3).abs() / self.get_gravitational_parameter()).sqrt()
1638    }
1639
1640    /// Gets the time when the orbit is in apoapsis, in seconds since epoch.
1641    ///
1642    /// This returns the time when mean anomaly equals pi.  
1643    /// This means although it will represent a time of apoapsis,
1644    /// it doesn't mean "next apoapsis" nor "previous apoapsis",
1645    /// it just means "an apoapsis", at least for closed orbits
1646    /// (e < 1).
1647    ///
1648    /// # Performance
1649    /// This function is performant and is unlikely to be the cause
1650    /// of any performance issues.
1651    ///
1652    /// # Example
1653    /// ```
1654    /// use keplerian_sim::{Orbit, OrbitTrait};
1655    ///
1656    /// const APOAPSIS: f64 = 2.0;
1657    /// const PERIAPSIS: f64 = 1.0;
1658    ///
1659    /// let orbit = Orbit::new_flat_with_apoapsis(
1660    ///     APOAPSIS,
1661    ///     PERIAPSIS,
1662    ///     2.9, // Argument of periapsis
1663    ///     1.5, // Mean anomaly at epoch
1664    ///     1.0, // Gravitational parameter
1665    /// );
1666    ///
1667    /// let time_of_ap = orbit.get_time_of_apoapsis();
1668    ///
1669    /// let alt_of_ap = orbit.get_altitude_at_time(time_of_ap);
1670    ///
1671    /// assert!(
1672    ///     (alt_of_ap - APOAPSIS).abs() < 1e-15
1673    /// );
1674    /// ```
1675    fn get_time_of_apoapsis(&self) -> f64 {
1676        // We want to find M = 0
1677        // Per `get_mean_anomaly_at_time`:
1678        // M = t * sqrt(mu / |a^3|) + M_0
1679        // => pi = t * sqrt(mu / |a^3|) + M_0
1680        // => pi - M_0 = t * sqrt(mu / |a^3|)
1681        // => t * sqrt(mu / |a^3|) = pi - M_0
1682        // => t = (pi - M_0) / sqrt(mu / |a^3|)
1683        //
1684        // note: 1 / sqrt(mu / |a^3|) = sqrt(|a^3| / mu)
1685        //
1686        // => t = (pi - M_0) * sqrt(|a^3| / mu)
1687        (PI - self.get_mean_anomaly_at_epoch())
1688            * (self.get_semi_major_axis().powi(3) / self.get_gravitational_parameter()).sqrt()
1689    }
1690
1691    /// Gets the transformation matrix needed to tilt a 2D vector into the
1692    /// tilted orbital plane.
1693    ///
1694    /// # Performance
1695    /// For [`CompactOrbit`], this will perform a few trigonometric operations.  
1696    /// If you need this value often, consider using [the cached orbit struct][crate::Orbit] instead.
1697    ///
1698    /// # Example
1699    /// ```
1700    /// use keplerian_sim::{Orbit, OrbitTrait};
1701    ///
1702    /// let orbit = Orbit::default();
1703    /// let matrix = orbit.get_transformation_matrix();
1704    ///
1705    /// assert_eq!(matrix, keplerian_sim::Matrix3x2 {
1706    ///     e11: 1.0, e12: 0.0,
1707    ///     e21: 0.0, e22: 1.0,
1708    ///     e31: 0.0, e32: 0.0,
1709    /// });
1710    /// ```
1711    fn get_transformation_matrix(&self) -> Matrix3x2;
1712
1713    /// Gets the basis vectors for the perifocal coordinate (PQW)
1714    /// system.
1715    ///
1716    /// # Output
1717    /// This function returns a tuple of three vectors. The vectors
1718    /// are the p, q, and w basis vectors, respectively.
1719    ///
1720    /// The p basis vector is a unit vector that points to the periapsis.  
1721    /// The q basis vector is orthogonal to that and points 90° counterclockwise
1722    /// from the periapsis on the orbital plane.  
1723    /// The w basis vector is orthogonal to the orbital plane.
1724    ///
1725    /// For more information about the PQW system, visit the
1726    /// [Wikipedia article](https://en.wikipedia.org/wiki/Perifocal_coordinate_system).
1727    ///
1728    /// # Performance
1729    /// For [`CompactOrbit`], this will perform a few trigonometric operations
1730    /// and multiplications, and therefore is not too performant.  
1731    ///
1732    /// For [`Orbit`], this will only need to compute a cross product, and
1733    /// therefore is much more performant.
1734    ///
1735    /// # Example
1736    /// ```
1737    /// use keplerian_sim::{Orbit, OrbitTrait};
1738    /// use glam::DVec3;
1739    ///
1740    /// let orbit = Orbit::default();
1741    /// let (p, q, w) = orbit.get_pqw_basis_vectors();
1742    ///
1743    /// assert_eq!(p, DVec3::X);
1744    /// assert_eq!(q, DVec3::Y);
1745    /// assert_eq!(w, DVec3::Z);
1746    /// ```
1747    fn get_pqw_basis_vectors(&self) -> (DVec3, DVec3, DVec3) {
1748        let mat = self.get_transformation_matrix();
1749
1750        let p = DVec3::new(mat.e11, mat.e21, mat.e31);
1751        let q = DVec3::new(mat.e12, mat.e22, mat.e32);
1752        let w = p.cross(q);
1753
1754        (p, q, w)
1755    }
1756
1757    /// Gets the p basis vector for the perifocal coordinate (PQW)
1758    /// system.
1759    ///
1760    /// The p basis vector is a unit vector that points to the periapsis.  
1761    ///
1762    /// For more information about the PQW system, visit the
1763    /// [Wikipedia article](https://en.wikipedia.org/wiki/Perifocal_coordinate_system).
1764    ///
1765    /// # Performance
1766    /// For [`CompactOrbit`], this will perform a few trigonometric operations
1767    /// and multiplications, and therefore is not too performant.  
1768    ///
1769    /// For [`Orbit`], this will only need to access the cache, and
1770    /// therefore is much more performant.
1771    ///
1772    /// # Example
1773    /// ```
1774    /// use keplerian_sim::{Orbit, CompactOrbit, OrbitTrait};
1775    /// use glam::DVec3;
1776    ///
1777    /// let orbit = Orbit::default();
1778    /// let p = orbit.get_pqw_basis_vector_p();
1779    /// let q = orbit.get_pqw_basis_vector_q();
1780    /// let w = orbit.get_pqw_basis_vector_w();
1781    ///
1782    /// assert_eq!(p, DVec3::X);
1783    /// assert_eq!(q, DVec3::Y);
1784    /// assert_eq!(w, DVec3::Z);
1785    ///
1786    /// let compact_orbit = CompactOrbit::default();
1787    /// let p = compact_orbit.get_pqw_basis_vector_p();
1788    /// let q = compact_orbit.get_pqw_basis_vector_q();
1789    /// let w = compact_orbit.get_pqw_basis_vector_w();
1790    ///
1791    /// assert_eq!(p, DVec3::X);
1792    /// assert_eq!(q, DVec3::Y);
1793    /// assert_eq!(w, DVec3::Z);
1794    /// ```
1795    fn get_pqw_basis_vector_p(&self) -> DVec3;
1796
1797    /// Gets the q basis vector for the perifocal coordinate (PQW)
1798    /// system.
1799    ///
1800    /// The q basis vector is orthogonal to the p basis vector
1801    /// and points 90° counterclockwise from the periapsis on the
1802    /// orbital plane.  
1803    ///
1804    /// For more information about the PQW system, visit the
1805    /// [Wikipedia article](https://en.wikipedia.org/wiki/Perifocal_coordinate_system).
1806    ///
1807    /// # Performance
1808    /// For [`CompactOrbit`], this will perform a few trigonometric operations
1809    /// and multiplications, and therefore is not too performant.  
1810    ///
1811    /// For [`Orbit`], this will only need to access the cache, and
1812    /// therefore is much more performant.
1813    ///
1814    /// # Example
1815    /// ```
1816    /// use keplerian_sim::{Orbit, CompactOrbit, OrbitTrait};
1817    /// use glam::DVec3;
1818    ///
1819    /// let orbit = Orbit::default();
1820    /// let p = orbit.get_pqw_basis_vector_p();
1821    /// let q = orbit.get_pqw_basis_vector_q();
1822    /// let w = orbit.get_pqw_basis_vector_w();
1823    ///
1824    /// assert_eq!(p, DVec3::X);
1825    /// assert_eq!(q, DVec3::Y);
1826    /// assert_eq!(w, DVec3::Z);
1827    ///
1828    /// let compact_orbit = CompactOrbit::default();
1829    /// let p = compact_orbit.get_pqw_basis_vector_p();
1830    /// let q = compact_orbit.get_pqw_basis_vector_q();
1831    /// let w = compact_orbit.get_pqw_basis_vector_w();
1832    ///
1833    /// assert_eq!(p, DVec3::X);
1834    /// assert_eq!(q, DVec3::Y);
1835    /// assert_eq!(w, DVec3::Z);
1836    /// ```
1837    fn get_pqw_basis_vector_q(&self) -> DVec3;
1838
1839    /// Gets the w basis vector for the perifocal coordinate (PQW)
1840    /// system.
1841    ///
1842    /// The w basis vector is orthogonal to the orbital plane.
1843    ///
1844    /// For more information about the PQW system, visit the
1845    /// [Wikipedia article](https://en.wikipedia.org/wiki/Perifocal_coordinate_system).
1846    ///
1847    /// # Performance
1848    /// For [`CompactOrbit`], this will perform a few trigonometric operations
1849    /// and multiplications, and therefore is not too performant.  
1850    ///
1851    /// For [`Orbit`], this will only need to compute a cross product, and
1852    /// therefore is much more performant.
1853    ///
1854    /// # Example
1855    /// ```
1856    /// use keplerian_sim::{Orbit, CompactOrbit, OrbitTrait};
1857    /// use glam::DVec3;
1858    ///
1859    /// let orbit = Orbit::default();
1860    /// let p = orbit.get_pqw_basis_vector_p();
1861    /// let q = orbit.get_pqw_basis_vector_q();
1862    /// let w = orbit.get_pqw_basis_vector_w();
1863    ///
1864    /// assert_eq!(p, DVec3::X);
1865    /// assert_eq!(q, DVec3::Y);
1866    /// assert_eq!(w, DVec3::Z);
1867    ///
1868    /// let compact_orbit = CompactOrbit::default();
1869    /// let p = compact_orbit.get_pqw_basis_vector_p();
1870    /// let q = compact_orbit.get_pqw_basis_vector_q();
1871    /// let w = compact_orbit.get_pqw_basis_vector_w();
1872    ///
1873    /// assert_eq!(p, DVec3::X);
1874    /// assert_eq!(q, DVec3::Y);
1875    /// assert_eq!(w, DVec3::Z);
1876    /// ```
1877    fn get_pqw_basis_vector_w(&self) -> DVec3;
1878
1879    /// Gets the eccentricity vector of this orbit.
1880    ///
1881    /// The eccentricity vector of a Kepler orbit is the dimensionless vector
1882    /// with direction pointing from apoapsis to periapsis and with magnitude
1883    /// equal to the orbit's scalar eccentricity.
1884    ///
1885    /// \- [Wikipedia](https://en.wikipedia.org/wiki/Eccentricity_vector)
1886    ///
1887    /// # Performance
1888    /// This function is significantly faster in the cached version of the
1889    /// orbit struct ([`Orbit`]) than the compact version ([`CompactOrbit`]).  
1890    /// Consider using the cached version if this function will be called often.
1891    ///
1892    /// Alternatively, if you want to keep using the compact version and know
1893    /// the periapsis unit vector, use the unchecked version:
1894    /// [`get_eccentricity_vector_unchecked`][OrbitTrait::get_eccentricity_vector_unchecked]
1895    ///
1896    /// The cached version only needs to do a multiplication, and therefore is
1897    /// very performant.
1898    ///
1899    /// The compact version additionally has to compute many multiplications,
1900    /// additions, and several trig operations.
1901    ///
1902    /// # Example
1903    /// ```
1904    /// use keplerian_sim::{Orbit, OrbitTrait};
1905    /// use glam::DVec3;
1906    ///
1907    /// // Parabolic orbit (e = 1)
1908    /// let orbit = Orbit::new_flat(1.0, 1.0, 0.0, 0.0, 1.0);
1909    /// let eccentricity_vector = orbit.get_eccentricity_vector();
1910    ///
1911    /// assert_eq!(
1912    ///     eccentricity_vector,
1913    ///     DVec3::X
1914    /// );
1915    /// ```
1916    fn get_eccentricity_vector(&self) -> DVec3 {
1917        self.get_eccentricity_vector_unchecked(self.get_pqw_basis_vectors().0)
1918    }
1919
1920    /// Gets the eccentricity vector of this orbit.
1921    ///
1922    /// The eccentricity vector of a Kepler orbit is the dimensionless vector
1923    /// with direction pointing from apoapsis to periapsis and with magnitude
1924    /// equal to the orbit's scalar eccentricity.
1925    ///
1926    /// \- [Wikipedia](https://en.wikipedia.org/wiki/Eccentricity_vector)
1927    ///
1928    /// # Unchecked Operation
1929    /// This function does not check for the validity of the given
1930    /// P basis vector. The given P vector should be of length 1.
1931    ///
1932    /// It is expected that callers get this basis vector
1933    /// from either the transformation matrix or the
1934    /// [`get_pqw_basis_vectors`][OrbitTrait::get_pqw_basis_vectors]
1935    /// function.
1936    ///
1937    /// If the given P vector is not of length 1, you may get
1938    /// nonsensical outputs.
1939    ///
1940    /// # Performance
1941    /// This function, by itself, is very performant, and should not be
1942    /// the cause of any performance problems.
1943    ///
1944    /// However, for the cached orbit struct ([`Orbit`]), this function
1945    /// has the same performance as the safer
1946    /// [`get_eccentricity_vector`][OrbitTrait::get_eccentricity_vector]
1947    /// function. There should be no need to use this function if you are
1948    /// using the cached orbit struct.
1949    ///
1950    /// # Example
1951    /// ```
1952    /// use keplerian_sim::{CompactOrbit, OrbitTrait};
1953    /// use glam::DVec3;
1954    ///
1955    /// // Parabolic orbit (e = 1)
1956    /// let orbit = CompactOrbit::new_flat(1.0, 1.0, 0.0, 0.0, 1.0);
1957    ///
1958    /// // Expensive op for compact orbit: get basis vectors
1959    /// let basis_vectors = orbit.get_pqw_basis_vectors();
1960    ///
1961    /// // Use basis vectors for something...
1962    /// assert_eq!(
1963    ///     basis_vectors,
1964    ///     (DVec3::X, DVec3::Y, DVec3::Z)
1965    /// );
1966    ///
1967    /// // You can reuse it here! No need to recompute (as long as
1968    /// // orbit hasn't changed)
1969    /// let eccentricity_vector = orbit.get_eccentricity_vector_unchecked(
1970    ///     // basis vectors: P, Q, and W; we get the first one (0th index)
1971    ///     basis_vectors.0
1972    /// );
1973    ///
1974    /// assert_eq!(
1975    ///     eccentricity_vector,
1976    ///     DVec3::X
1977    /// );
1978    /// ```
1979    fn get_eccentricity_vector_unchecked(&self, p_vector: DVec3) -> DVec3 {
1980        self.get_eccentricity() * p_vector
1981    }
1982
1983    /// Gets the longitude of periapsis of this orbit.
1984    ///
1985    /// The longitude of the periapsis, also called longitude of the pericenter,
1986    /// of an orbiting body is the longitude (measured from the point of the
1987    /// vernal equinox) at which the periapsis (closest approach to the
1988    /// central body) would occur if the body's orbit inclination were zero.  
1989    /// It is usually denoted ϖ.
1990    ///
1991    /// \- [Wikipedia](https://en.wikipedia.org/wiki/Longitude_of_periapsis)
1992    ///
1993    /// # Performance
1994    /// This function is very performant and should not be the cause
1995    /// of any performance problems.
1996    fn get_longitude_of_periapsis(&self) -> f64 {
1997        self.get_arg_pe() + self.get_long_asc_node()
1998    }
1999
2000    /// Gets the true longitude at a true anomaly in the orbit.
2001    ///
2002    /// True longitude is the ecliptic longitude at which an
2003    /// orbiting body could actually be found if its inclination were zero.
2004    ///
2005    /// \- [Wikipedia](https://en.wikipedia.org/wiki/True_longitude)
2006    ///
2007    /// # Performance
2008    /// This function is very performant and should not be the cause
2009    /// of any performance problems.
2010    fn get_true_longitude_at_true_anomaly(&self, true_anomaly: f64) -> f64 {
2011        true_anomaly + self.get_longitude_of_periapsis()
2012    }
2013
2014    /// Gets the true anomaly at the ascending node with respect to the
2015    /// XY plane (at Z = 0).
2016    ///
2017    /// An orbital node is either of the two points where an orbit intersects
2018    /// a plane of reference to which it is inclined.
2019    ///
2020    /// \- [Wikipedia](https://en.wikipedia.org/wiki/Orbital_node)
2021    ///
2022    /// If you want the descending node as well, it is faster to use the
2023    /// following formula than to call the corresponding descending node
2024    /// function:
2025    /// ```
2026    /// # use core::f64::consts::{PI, TAU};
2027    /// # let f_AN = 0.42;
2028    /// # let
2029    /// f_DN = (f_AN + PI).rem_euclid(TAU)
2030    /// # ;
2031    /// ```
2032    ///
2033    /// If you want the ascending node with respect to another plane, use
2034    /// [`get_true_anomaly_at_asc_node_with_plane`][OrbitTrait::get_true_anomaly_at_asc_node_with_plane]
2035    /// instead.
2036    ///
2037    /// # True anomaly domain
2038    /// In the case of open (parabolic/hyperbolic) trajectories, this function
2039    /// can return a true anomaly outside the valid range. This indicates
2040    /// that that open trajectory does not have an AN/DN crossing.
2041    ///
2042    /// It is the caller's job to check for itself whether this true anomaly is
2043    /// within the valid range. This can be done using the
2044    /// [`get_true_anomaly_at_asymptote`][OrbitTrait::get_true_anomaly_at_asymptote]
2045    /// function.
2046    ///
2047    /// This out-of-range issue does not appear in closed (elliptic) orbits.
2048    ///
2049    /// # Performance
2050    /// This function is very performant and should not be the cause
2051    /// of any performance problems.
2052    ///
2053    /// This function is significantly faster than the general-plane function
2054    /// [`get_true_anomaly_at_asc_node_with_plane`][OrbitTrait::get_true_anomaly_at_asc_node_with_plane]
2055    /// as it is more specialized for this calculation.
2056    ///
2057    /// # Example
2058    /// ```
2059    /// use keplerian_sim::{Orbit, OrbitTrait};
2060    ///
2061    /// # fn main() {
2062    /// let orbit = Orbit::new_circular(
2063    ///     1.0, // radius
2064    ///     1.0, // inclination
2065    ///     2.0, // longitude of AN
2066    ///     0.0, // mean anomaly (irrelevant)
2067    ///     1.0, // mu (irrelevant)
2068    /// );
2069    ///
2070    /// let f_an = orbit.get_true_anomaly_at_asc_node();
2071    ///
2072    /// assert!(
2073    ///     orbit.get_velocity_at_true_anomaly(f_an).z > 0.0
2074    /// );
2075    ///
2076    /// let f_dn = (f_an + std::f64::consts::PI)
2077    ///     .rem_euclid(std::f64::consts::TAU);
2078    ///
2079    /// assert!(
2080    ///     orbit.get_velocity_at_true_anomaly(f_dn).z < 0.0
2081    /// );
2082    /// # }
2083    /// ```
2084    fn get_true_anomaly_at_asc_node(&self) -> f64 {
2085        // true anomaly `f` of one of the nodes.
2086        // we don't know if this is AN or DN yet.
2087        let node_f = -self.get_arg_pe();
2088
2089        if self.get_inclination().rem_euclid(TAU) < PI {
2090            node_f.rem_euclid(TAU)
2091        } else {
2092            (node_f + PI).rem_euclid(TAU)
2093        }
2094    }
2095
2096    /// Gets the true anomaly at the descending node with respect to the
2097    /// XY plane (at Z = 0).
2098    ///
2099    /// An orbital node is either of the two points where an orbit intersects
2100    /// a plane of reference to which it is inclined.
2101    ///
2102    /// \- [Wikipedia](https://en.wikipedia.org/wiki/Orbital_node)
2103    ///
2104    /// If you want the ascending node as well, it is faster to use the
2105    /// following formula than to call the corresponding descending node
2106    /// function:
2107    /// ```
2108    /// # use core::f64::consts::{PI, TAU};
2109    /// # let f_DN = 0.42;
2110    /// # let
2111    /// f_AN = (f_DN + PI).rem_euclid(TAU)
2112    /// # ;
2113    /// ```
2114    ///
2115    /// If you want the descending node with respect to another plane, use
2116    /// [`get_true_anomaly_at_desc_node_with_plane`][OrbitTrait::get_true_anomaly_at_desc_node_with_plane]
2117    /// instead.
2118    ///
2119    /// # True anomaly domain
2120    /// In the case of open (parabolic/hyperbolic) trajectories, this function
2121    /// can return a true anomaly outside the valid range. This indicates
2122    /// that that open trajectory does not have an AN/DN crossing.
2123    ///
2124    /// It is the caller's job to check for itself whether this true anomaly is
2125    /// within the valid range. This can be done using the
2126    /// [`get_true_anomaly_at_asymptote`][OrbitTrait::get_true_anomaly_at_asymptote]
2127    /// function.
2128    ///
2129    /// This out-of-range issue does not appear in closed (elliptic) orbits.
2130    ///
2131    /// # Performance
2132    /// This function is very performant and should not be the cause
2133    /// of any performance problems.
2134    ///
2135    /// This function is significantly faster than the general-plane function
2136    /// [`get_true_anomaly_at_desc_node_with_plane`][OrbitTrait::get_true_anomaly_at_desc_node_with_plane]
2137    /// as it is more specialized for this calculation.
2138    ///
2139    /// # Example
2140    /// ```
2141    /// use keplerian_sim::{Orbit, OrbitTrait};
2142    ///
2143    /// # fn main() {
2144    /// let orbit = Orbit::new_circular(
2145    ///     1.0, // radius
2146    ///     1.0, // inclination
2147    ///     2.0, // longitude of AN
2148    ///     0.0, // mean anomaly (irrelevant)
2149    ///     1.0, // mu (irrelevant)
2150    /// );
2151    ///
2152    /// let f_dn = orbit.get_true_anomaly_at_desc_node();
2153    ///
2154    /// assert!(
2155    ///     orbit.get_velocity_at_true_anomaly(f_dn).z < 0.0
2156    /// );
2157    ///
2158    /// let f_an = (f_dn + std::f64::consts::PI)
2159    ///     .rem_euclid(std::f64::consts::TAU);
2160    ///
2161    /// assert!(
2162    ///     orbit.get_velocity_at_true_anomaly(f_an).z > 0.0
2163    /// );
2164    /// # }
2165    /// ```
2166    fn get_true_anomaly_at_desc_node(&self) -> f64 {
2167        // true anomaly `f` of one of the nodes.
2168        // we don't know if this is AN or DN yet.
2169        let node_f = -self.get_arg_pe();
2170
2171        if self.get_inclination().rem_euclid(TAU) < PI {
2172            (node_f + PI).rem_euclid(TAU)
2173        } else {
2174            node_f.rem_euclid(TAU)
2175        }
2176    }
2177
2178    /// Gets the true anomaly of an ascending node, given a reference plane's
2179    /// normal vector.
2180    ///
2181    /// This can be used to get the ascending node compared to another orbit,
2182    /// similar to the AN/DN labels that appear in Kerbal Space Program after you
2183    /// pick a target vessel/body.
2184    ///
2185    /// **You will get a NaN if the plane normals of the
2186    /// two orbits match exactly.** In that scenario there is no ascending
2187    /// nor descending node.
2188    ///
2189    /// To do that, you can get the plane normal of the other orbit using
2190    /// [`get_pqw_basis_vectors`][OrbitTrait::get_pqw_basis_vectors],
2191    /// then feed that into the original orbit's ascending node getter.
2192    ///
2193    /// # Unchecked Operation
2194    /// It is the caller's job to make sure that the given
2195    /// plane normal is of length 1.  
2196    /// If the given plane normal is not of length 1, you may get nonsensical
2197    /// outputs.
2198    ///
2199    /// # Performance
2200    /// This function is moderately faster in the cached version of the
2201    /// orbit struct ([`Orbit`]) than the compact version ([`CompactOrbit`]).  
2202    /// Consider using the cached version if this function will be called often.
2203    ///
2204    /// The cached version only needs to do a cross-product, and therefore is
2205    /// very performant.
2206    ///
2207    /// The compact version additionally has to compute many multiplications,
2208    /// additions, and several trig operations.
2209    ///
2210    /// Note that if you want to get both the ascending node
2211    /// and descending node, you can use the equality below to compute
2212    /// the descending node from the ascending node.
2213    /// This is far more performant than calling this function and
2214    /// [`get_true_anomaly_at_desc_node_with_plane`][OrbitTrait::get_true_anomaly_at_desc_node_with_plane]
2215    /// separately.
2216    ///
2217    /// ```
2218    /// # use core::f64::consts::{PI, TAU};
2219    /// # let f_AN = 0.42;
2220    /// # let
2221    /// f_DN = (f_AN + PI).rem_euclid(TAU)
2222    /// # ;
2223    /// ```
2224    ///
2225    /// Also, if you only need the ascending node with respect to the XY
2226    /// plane (with a +Z normal vector), consider using the specialized
2227    /// [`get_true_anomaly_at_asc_node`][OrbitTrait::get_true_anomaly_at_asc_node]
2228    /// function instead as it is much more performant.
2229    ///
2230    /// # True anomaly domain
2231    /// In the case of open (parabolic/hyperbolic) trajectories, this function
2232    /// can return a true anomaly outside the valid range. This indicates
2233    /// that that open trajectory does not have an AN/DN crossing.
2234    ///
2235    /// It is the caller's job to check for itself whether this true anomaly is
2236    /// within the valid range. This can be done using the
2237    /// [`get_true_anomaly_at_asymptote`][OrbitTrait::get_true_anomaly_at_asymptote]
2238    /// function.
2239    ///
2240    /// This out-of-range issue does not appear in closed (elliptic) orbits.
2241    ///
2242    /// # Example
2243    /// ```
2244    /// use keplerian_sim::{Orbit, OrbitTrait};
2245    /// use std::f64::consts::{PI, TAU};
2246    ///
2247    /// fn assert_almost_eq(a: f64, b: f64) {
2248    ///     assert!((a - b).abs() < 1e-13, "{a} != {b}");
2249    /// }
2250    ///
2251    /// let this_orbit = Orbit::new(
2252    ///     0.8, // Eccentricity
2253    ///     28.4, // Periapsis
2254    ///     1.98, // Inclination
2255    ///     2.91, // Argument of periapsis
2256    ///     0.50, // Longitude of ascending node (rel. to XY plane)
2257    ///     2.9, // Mean anomaly at epoch
2258    ///     1.0, // Gravitational parameter
2259    /// );
2260    ///
2261    /// let other_orbit = Orbit::new(
2262    ///     0.6, // Eccentricity
2263    ///     11.0, // Periapsis
2264    ///     3.01, // Inclination
2265    ///     1.59, // Argument of periapsis
2266    ///     0.44, // Longitude of ascending node (rel. to XY plane)
2267    ///     1.25, // Mean anomaly at epoch
2268    ///     1.0, // Gravitational parameter
2269    /// );
2270    ///
2271    /// let this_normal = this_orbit.get_pqw_basis_vectors().2;
2272    /// let other_normal = other_orbit.get_pqw_basis_vectors().2;
2273    ///
2274    /// let this_an = this_orbit.get_true_anomaly_at_asc_node_with_plane(other_normal);
2275    /// let this_dn = (this_an + PI).rem_euclid(TAU);
2276    /// assert_almost_eq(this_an, 0.2224161750528122);
2277    /// assert_almost_eq(this_dn, 3.3640088286426053);
2278    ///
2279    /// let other_an = other_orbit.get_true_anomaly_at_asc_node_with_plane(this_normal);
2280    /// let other_dn = (other_an + PI).rem_euclid(TAU);
2281    /// assert_almost_eq(other_an, 4.628980494949403);
2282    /// assert_almost_eq(other_dn, 1.4873878413596096);
2283    /// ```
2284    fn get_true_anomaly_at_asc_node_with_plane(&self, plane_normal: DVec3) -> f64 {
2285        // We first get the line of nodes relative to the new
2286        // plane normal instead of the one we store (which is relative to the
2287        // XY plane with a normal of +Z).
2288        //
2289        // https://orbital-mechanics.space/classical-orbital-elements/orbital-elements-and-the-state-vector.html#step-4right-ascension-of-the-ascending-node
2290        //
2291        //     vec_N = vec_K × vec_h
2292        //
2293        // ...where:
2294        // vec_N = Line of nodes <https://en.wikipedia.org/wiki/Line_of_nodes>
2295        // vec_K = plane normal (usually XZ-plane = Z-up, now custom-defined)
2296        // vec_h = well..
2297        //     In the website they use a specific angular momentum.
2298        //     In this case however, the w-hat basis vector
2299        //     (in the PQW coordinate system) can be used instead
2300        //     since we kinda normalize it anyway in the next steps,
2301        //     so it all works out.
2302        let (basis_p, _, basis_w) = self.get_pqw_basis_vectors();
2303        let line_of_nodes = plane_normal.cross(basis_w);
2304
2305        // Then we can calculate the argument of periapsis.
2306        // https://orbital-mechanics.space/classical-orbital-elements/orbital-elements-and-the-state-vector.html#step-6argument-of-periapsis
2307        //
2308        //    ω_pre = cos^-1(vec_e ⋅ vec_N / e ||vec_N||)
2309        //
2310        // ...where:
2311        // vec_e = Eccentricity vector
2312        // vec_N = Line of nodes
2313        // e = Eccentricity scalar
2314        //
2315        // We can simplify and generalize this to make it compatible with
2316        // the case where e = 0.
2317        //
2318        // From `vec_e := e * \hat{p}`,
2319        // notice that in the ω_pre equation we have `vec_e / e`.
2320        // This can be transformed into just `\hat{p}` which is much more stable.
2321        //
2322        // Therefore:
2323        //
2324        //    ω_pre = cos^-1(\hat{p} ⋅ vec_N / ||vec_N||)
2325        //
2326        // ...where:
2327        // \hat{p} = P basis vector in PQW coordinate system
2328
2329        let arg_pe_pre = (basis_p.dot(line_of_nodes.normalize())).acos();
2330
2331        // The next equation from the website basically states:
2332        //
2333        //      ω = {vec_e.z >= 0: ω_pre; τ - ω_pre}
2334        //
2335        // We can simplify `vec_e.z`:
2336        // vec_e := e * \hat{p}
2337        // => vec_e.z = e * \hat{p}.z
2338        //
2339        //      vec_e.z >= 0  ==>  e * \hat{p}.z >= 0
2340        //
2341        // Since eccentricity `e` is defined to be nonnegative,
2342        // and we only need the sign (`>= 0`), we can simplify
2343        // this further:
2344        //
2345        //      e * \hat{p}.z >= 0  ==>  \hat{p}.z >= 0
2346        //
2347        // Rewriting the original expression:
2348        //
2349        //      ω = {\hat{p}.z >= 0: ω_pre; τ - ω_pre}
2350        //
2351        // HOWEVER: This is only correct if we use the +Z vector as plane normal.
2352        // We'll want to project the \hat{p} vector into the plane normal instead:
2353        //
2354        //      ω = {\hat{p} ⋅ k >= 0: ω_pre; τ - ω_pre}
2355
2356        let arg_pe = if basis_p.dot(plane_normal) >= 0.0 {
2357            arg_pe_pre
2358        } else {
2359            TAU - arg_pe_pre
2360        };
2361
2362        // True anomaly `f` of one of the nodes.
2363        // We don't know if this is AN or DN yet.
2364        // We need the inclination relative to the reference plane to
2365        // find that out.
2366        let node_f = -arg_pe;
2367
2368        // Next we need the inclination relative to the reference plane.
2369        // Next equation from the same page as before, different section:
2370        // https://orbital-mechanics.space/classical-orbital-elements/orbital-elements-and-the-state-vector.html#step-3inclination
2371        //
2372        //      i = cos^-1(h_z / ||h||)
2373        //
2374        // which can be rearranged into:
2375        //
2376        //      i = cos^-1(\hat{w}.z)
2377        //
2378        // (We can replace normalized h with \hat{w}. See the first comment block
2379        // of this function for the reason why.)
2380        //
2381        // HOWEVER: This equation is for the inclination relative to the XY plane,
2382        // where its normal is the +Z unit vector.
2383        // We want to get the inclination relative to the given plane normal.
2384        //
2385        // Given that \hat{w}.z returns the same as \hat{w} ⋅ [0, 0, 1],
2386        // we can generalize this to project the \hat{w} vector into any
2387        // other vector instead of just the +Z unit vector.
2388        //
2389        // This means, to get the inclination relative to the reference plane,
2390        // we can dot it with the plane normal:
2391        //
2392        //      i = cos^-1(\hat{w} ⋅ k)
2393        //
2394        // ...where:
2395        // k = the unit normal vector of the reference plane (`plane_normal`).
2396
2397        let inclination = (basis_w.dot(plane_normal)).acos();
2398
2399        if inclination.rem_euclid(TAU) < PI {
2400            node_f.rem_euclid(TAU)
2401        } else {
2402            (node_f + PI).rem_euclid(TAU)
2403        }
2404    }
2405
2406    /// Gets the true anomaly of a descending node, given a reference plane's
2407    /// normal vector.
2408    ///
2409    /// This can be used to get the descending node compared to another orbit,
2410    /// similar to the AN/DN labels that appear in Kerbal Space Program after you
2411    /// pick a target vessel/body.
2412    ///
2413    /// **You will get a NaN if the plane normals of the
2414    /// two orbits match exactly.** In that scenario there is no ascending
2415    /// nor descending node.
2416    ///
2417    /// To do that, you can get the plane normal of the other orbit using
2418    /// [`get_pqw_basis_vectors`][OrbitTrait::get_pqw_basis_vectors],
2419    /// then feed that into the original orbit's descending node getter.
2420    ///
2421    /// # Unchecked Operation
2422    /// It is the caller's job to make sure that the given
2423    /// plane normal is of length 1.  
2424    /// If the given plane normal is not of length 1, you may get nonsensical
2425    /// outputs.
2426    ///
2427    /// # Performance
2428    /// This function is moderately faster in the cached version of the
2429    /// orbit struct ([`Orbit`]) than the compact version ([`CompactOrbit`]).  
2430    /// Consider using the cached version if this function will be called often.
2431    ///
2432    /// Note that if you want to get both the ascending node
2433    /// and descending node, you can use the equality below to compute
2434    /// the descending node from the ascending node.
2435    /// This is far more performant than calling this function and
2436    /// [`get_true_anomaly_at_asc_node_with_plane`][OrbitTrait::get_true_anomaly_at_asc_node_with_plane]
2437    /// separately.
2438    ///
2439    /// ```
2440    /// # use core::f64::consts::{PI, TAU};
2441    /// # let f_DN = 0.42;
2442    /// # let
2443    /// f_AN = (f_DN + PI).rem_euclid(TAU)
2444    /// # ;
2445    /// ```
2446    ///
2447    /// Also, if you only need the descending node with respect to the XY
2448    /// plane (with a +Z normal vector), consider using the specialized
2449    /// [`get_true_anomaly_at_desc_node`][OrbitTrait::get_true_anomaly_at_desc_node]
2450    /// function instead as it is much more performant.
2451    ///
2452    /// # True anomaly domain
2453    /// In the case of open (parabolic/hyperbolic) trajectories, this function
2454    /// can return a true anomaly outside the valid range. This indicates
2455    /// that that open trajectory does not have an AN/DN crossing.
2456    ///
2457    /// It is the caller's job to check for itself whether this true anomaly is
2458    /// within the valid range. This can be done using the
2459    /// [`get_true_anomaly_at_asymptote`][OrbitTrait::get_true_anomaly_at_asymptote]
2460    /// function.
2461    ///
2462    /// This out-of-range issue does not appear in closed (elliptic) orbits.
2463    ///
2464    /// # Example
2465    /// ```
2466    /// use keplerian_sim::{Orbit, OrbitTrait};
2467    /// use std::f64::consts::{PI, TAU};
2468    ///
2469    /// fn assert_almost_eq(a: f64, b: f64) {
2470    ///     assert!((a - b).abs() < 1e-13, "{a} != {b}");
2471    /// }
2472    ///
2473    /// let this_orbit = Orbit::new(
2474    ///     0.8, // Eccentricity
2475    ///     28.4, // Periapsis
2476    ///     1.98, // Inclination
2477    ///     2.91, // Argument of periapsis
2478    ///     0.50, // Longitude of ascending node (rel. to XY plane)
2479    ///     2.9, // Mean anomaly at epoch
2480    ///     1.0, // Gravitational parameter
2481    /// );
2482    ///
2483    /// let other_orbit = Orbit::new(
2484    ///     0.6, // Eccentricity
2485    ///     11.0, // Periapsis
2486    ///     3.01, // Inclination
2487    ///     1.59, // Argument of periapsis
2488    ///     0.44, // Longitude of ascending node (rel. to XY plane)
2489    ///     1.25, // Mean anomaly at epoch
2490    ///     1.0, // Gravitational parameter
2491    /// );
2492    ///
2493    /// let this_normal = this_orbit.get_pqw_basis_vectors().2;
2494    /// let other_normal = other_orbit.get_pqw_basis_vectors().2;
2495    ///
2496    /// let this_dn = this_orbit.get_true_anomaly_at_desc_node_with_plane(other_normal);
2497    /// let this_an = (this_dn + PI).rem_euclid(TAU);
2498    /// assert_almost_eq(this_an, 0.2224161750528122);
2499    /// assert_almost_eq(this_dn, 3.3640088286426053);
2500    ///
2501    /// let other_dn = other_orbit.get_true_anomaly_at_desc_node_with_plane(this_normal);
2502    /// let other_an = (other_dn + PI).rem_euclid(TAU);
2503    /// assert_almost_eq(other_an, 4.628980494949403);
2504    /// assert_almost_eq(other_dn, 1.4873878413596096);
2505    /// ```
2506    fn get_true_anomaly_at_desc_node_with_plane(&self, plane_normal: DVec3) -> f64 {
2507        // We first get the line of nodes relative to the new
2508        // plane normal instead of the one we store (which is relative to the
2509        // XY plane with a normal of +Z).
2510        //
2511        // https://orbital-mechanics.space/classical-orbital-elements/orbital-elements-and-the-state-vector.html#step-4right-ascension-of-the-ascending-node
2512        //
2513        //     vec_N = vec_K × vec_h
2514        //
2515        // ...where:
2516        // vec_N = Line of nodes <https://en.wikipedia.org/wiki/Line_of_nodes>
2517        // vec_K = plane normal (usually XZ-plane = Z-up, now custom-defined)
2518        // vec_h = well..
2519        //     In the website they use a specific angular momentum.
2520        //     In this case however, the w-hat basis vector
2521        //     (in the PQW coordinate system) can be used instead
2522        //     since we kinda normalize it anyway in the next steps,
2523        //     so it all works out.
2524        let (basis_p, _, basis_w) = self.get_pqw_basis_vectors();
2525        let line_of_nodes = plane_normal.cross(basis_w);
2526
2527        // Then we can calculate the argument of periapsis.
2528        // https://orbital-mechanics.space/classical-orbital-elements/orbital-elements-and-the-state-vector.html#step-6argument-of-periapsis
2529        //
2530        //    ω_pre = cos^-1(vec_e ⋅ vec_N / e ||vec_N||)
2531        //
2532        // ...where:
2533        // vec_e = Eccentricity vector
2534        // vec_N = Line of nodes
2535        // e = Eccentricity scalar
2536        //
2537        // We can simplify and generalize this to make it compatible with
2538        // the case where e = 0.
2539        //
2540        // From `vec_e := e * \hat{p}`,
2541        // notice that in the ω_pre equation we have `vec_e / e`.
2542        // This can be transformed into just `\hat{p}` which is much more stable.
2543        //
2544        // Therefore:
2545        //
2546        //    ω_pre = cos^-1(\hat{p} ⋅ vec_N / ||vec_N||)
2547        //
2548        // ...where:
2549        // \hat{p} = P basis vector in PQW coordinate system
2550
2551        let arg_pe_pre = (basis_p.dot(line_of_nodes.normalize())).acos();
2552
2553        // The next equation from the website basically states:
2554        //
2555        //      ω = {vec_e.z >= 0: ω_pre; τ - ω_pre}
2556        //
2557        // We can simplify `vec_e.z`:
2558        // vec_e := e * \hat{p}
2559        // => vec_e.z = e * \hat{p}.z
2560        //
2561        //      vec_e.z >= 0  ==>  e * \hat{p}.z >= 0
2562        //
2563        // Since eccentricity `e` is defined to be nonnegative,
2564        // and we only need the sign (`>= 0`), we can simplify
2565        // this further:
2566        //
2567        //      e * \hat{p}.z >= 0  ==>  \hat{p}.z >= 0
2568        //
2569        // Rewriting the original expression:
2570        //
2571        //      ω = {\hat{p}.z >= 0: ω_pre; τ - ω_pre}
2572        //
2573        // HOWEVER: This is only correct if we use the +Z vector as plane normal.
2574        // We'll want to project the \hat{p} vector into the plane normal instead:
2575        //
2576        //      ω = {\hat{p} ⋅ k >= 0: ω_pre; τ - ω_pre}
2577
2578        let arg_pe = if basis_p.dot(plane_normal) >= 0.0 {
2579            arg_pe_pre
2580        } else {
2581            TAU - arg_pe_pre
2582        };
2583
2584        // True anomaly `f` of one of the nodes.
2585        // We don't know if this is AN or DN yet.
2586        // We need the inclination relative to the reference plane to
2587        // find that out.
2588        let node_f = -arg_pe;
2589
2590        // Next we need the inclination relative to the reference plane.
2591        // Next equation from the same page as before, different section:
2592        // https://orbital-mechanics.space/classical-orbital-elements/orbital-elements-and-the-state-vector.html#step-3inclination
2593        //
2594        //      i = cos^-1(h_z / ||h||)
2595        //
2596        // which can be rearranged into:
2597        //
2598        //      i = cos^-1(\hat{w}.z)
2599        //
2600        // (We can replace normalized h with \hat{w}. See the first comment block
2601        // of this function for the reason why.)
2602        //
2603        // HOWEVER: This equation is for the inclination relative to the XY plane,
2604        // where its normal is the +Z unit vector.
2605        // We want to get the inclination relative to the given plane normal.
2606        //
2607        // Given that \hat{w}.z returns the same as \hat{w} ⋅ [0, 0, 1],
2608        // we can generalize this to project the \hat{w} vector into any
2609        // other vector instead of just the +Z unit vector.
2610        //
2611        // This means, to get the inclination relative to the reference plane,
2612        // we can dot it with the plane normal:
2613        //
2614        //      i = cos^-1(\hat{w} ⋅ k)
2615        //
2616        // ...where:
2617        // k = the unit normal vector of the reference plane (`plane_normal`).
2618
2619        let inclination = (basis_w.dot(plane_normal)).acos();
2620
2621        if inclination.rem_euclid(TAU) < PI {
2622            (node_f + PI).rem_euclid(TAU)
2623        } else {
2624            node_f.rem_euclid(TAU)
2625        }
2626    }
2627
2628    // TODO: POST-PARABOLIC SUPPORT: Add note about parabolic eccentric anomaly (?), remove parabolic support sections
2629    /// Gets the eccentric anomaly at a given mean anomaly in the orbit.
2630    ///
2631    /// When the orbit is open (has an eccentricity of at least 1),
2632    /// the [hyperbolic eccentric anomaly](https://space.stackexchange.com/questions/27602/what-is-hyperbolic-eccentric-anomaly-f)
2633    /// would be returned instead.
2634    ///
2635    /// # Parabolic Support
2636    /// This function doesn't yet support parabolic trajectories. It may return `NaN`s
2637    /// or nonsensical values.
2638    ///
2639    /// # Performance
2640    /// The method to get the eccentric anomaly from the mean anomaly
2641    /// uses numerical approach methods, and so it is not performant.  
2642    /// It is recommended to cache this value if you can.
2643    ///
2644    /// The eccentric anomaly is an angular parameter that defines the position
2645    /// of a body that is moving along an elliptic Kepler orbit.
2646    ///
2647    /// — [Wikipedia](https://en.wikipedia.org/wiki/Eccentric_anomaly)
2648    fn get_eccentric_anomaly_at_mean_anomaly(&self, mean_anomaly: f64) -> f64 {
2649        // TODO: PARABOLIC SUPPORT: This function doesn't consider parabolic support yet.
2650        if self.get_eccentricity() < 1.0 {
2651            self.get_elliptic_eccentric_anomaly(mean_anomaly)
2652        } else {
2653            self.get_hyperbolic_eccentric_anomaly(mean_anomaly)
2654        }
2655    }
2656
2657    /// Get an initial guess for the hyperbolic eccentric anomaly of an orbit.
2658    ///
2659    /// # Performance
2660    /// This function uses plenty of floating-point operations, including
2661    /// divisions, natural logarithms, squareroots, and cuberoots, and thus
2662    /// it is not very performant.
2663    ///
2664    /// # Unchecked Operation
2665    /// This function does not check whether or not the orbit is hyperbolic. If
2666    /// this function is called on a non-hyperbolic orbit (i.e., elliptic or parabolic),
2667    /// invalid values may be returned.
2668    ///
2669    /// # Approximate Guess
2670    /// This function returns a "good" initial guess for the hyperbolic eccentric anomaly.  
2671    /// There are no constraints on the accuracy of the guess, and users may not
2672    /// rely on this value being very accurate, especially in some edge cases.
2673    ///
2674    /// # Source
2675    /// From the paper:  
2676    /// "A new method for solving the hyperbolic Kepler equation"  
2677    /// by Baisheng Wu et al.  
2678    /// Quote:
2679    /// "we divide the hyperbolic eccentric anomaly interval into two parts:
2680    /// a finite interval and an infinite interval. For the finite interval,
2681    /// we apply a piecewise Pade approximation to establish an initial
2682    /// approximate solution of HKE. For the infinite interval, an analytical
2683    /// initial approximate solution is constructed."
2684    fn get_approx_hyperbolic_eccentric_anomaly(&self, mean_anomaly: f64) -> f64 {
2685        let sign = mean_anomaly.signum();
2686        let mean_anomaly = mean_anomaly.abs();
2687        const SINH_5: f64 = 74.20321057778875;
2688
2689        let eccentricity = self.get_eccentricity();
2690
2691        // (Paragraph after Eq. 5 in the aforementioned paper)
2692        //   The [mean anomaly] interval [0, e_c sinh(5) - 5) can
2693        //   be separated into fifteen subintervals corresponding to
2694        //   those intervals of F in [0, 5), see Eq. (4).
2695        sign * if mean_anomaly < eccentricity * SINH_5 - 5.0 {
2696            // We use the Pade approximation of sinh of order
2697            // [3 / 2], in `crate::generated_sinh_approximator`.
2698            // We can then rearrange the equation to a cubic
2699            // equation in terms of (F - a) and solve it.
2700            //
2701            // To quote the paper:
2702            //   Replacing sinh(F) in [the hyperbolic Kepler
2703            //   equation] with its piecewise Pade approximation
2704            //   defined in Eq. (4) [`crate::generated_sinh_approximator`]
2705            //   yields:
2706            //     e_c P(F) - F = M_h                          (6)
2707            //
2708            //   Eq. (6) can be written as a cubic equation in u = F - a, as
2709            //     (e_c p_3 - q_2)u^3 +
2710            //     (e_c p_2 - (M_h + a)q_2 - q_1) u^2 +
2711            //     (e_c p_1 - (M_h + a)q_1 - 1)u +
2712            //     e_c s - M_h - a = 0                         (7)
2713            //
2714            //   Solving Eq. (7) and picking the real root F = F_0 in the
2715            //   corresponding subinterval results in an initial approximate
2716            //   solution to [the hyperbolic Kepler equation].
2717            //
2718            // For context:
2719            // - `e_c` is eccentricity
2720            // - `p_*`, `q_*`, `a`, and `s` is derived from the Pade approximation
2721            //   arguments, which can be retrieved using the
2722            //   `generated_sinh_approximator::get_sinh_approx_params` function
2723            // - `M_h` is the mean anomaly
2724            // - `F` is the eccentric anomaly
2725
2726            use crate::generated_sinh_approximator::get_sinh_approx_params;
2727            let params = get_sinh_approx_params(mean_anomaly);
2728
2729            // We first get the value of each coefficient in the cubic equation:
2730            // Au^3 + Bu^2 + Cu + D = 0
2731            let mean_anom_plus_a = mean_anomaly + params.a;
2732            let coeff_a = eccentricity * params.p_3 - params.q_2;
2733            let coeff_b = eccentricity * params.p_2 - mean_anom_plus_a * params.q_2 - params.q_1;
2734            let coeff_c = eccentricity * params.p_1 - mean_anom_plus_a * params.q_1 - 1.0;
2735            let coeff_d = eccentricity * params.s - mean_anomaly - params.a;
2736
2737            // Then we solve it to get the value of u = F - a
2738            let u = solve_monotone_cubic(coeff_a, coeff_b, coeff_c, coeff_d);
2739
2740            u + params.a
2741        } else {
2742            // Equation 13
2743            // A *very* rough guess, with an error that may exceed 1%.
2744            let rough_guess = (2.0 * mean_anomaly / eccentricity).ln();
2745
2746            /*
2747            A fourth-order Schröder iteration of the second kind
2748            is performed to create a better guess.
2749            ...Apparently it's not a well-known thing, but the aforementioned paper
2750            referenced this other paper about Schröder iterations:
2751            https://doi.org/10.1016/j.cam.2019.02.035
2752
2753            To do the Schröder iteration, we need to compute a delta value
2754            to be added to the rough guess. Part of Equation 15 from the paper is below.
2755
2756            delta = (
2757                    6 * [e_c^2 / (4 * M_h) + F_a] / (e_c * c_a - 1) +
2758                    3 * [e_c * s_a / (e_c * c_a - 1)]{[e_c^2 / (4 * M_h) + F_a] / (e_c * c_a - 1)}^2
2759                ) / (
2760                    6 +
2761                    6 * [e_c * s_a / (e_c * c_a - 1)]{[e_c^2 / (4 * M_h) + F_a] / (e_c * c_a - 1)} +
2762                    [e_c * c_a / (e_c * c_a - 1)]{[e_c^2 / (4 * M_h) + F_a] / (e_c * c_a - 1)}^2
2763                )
2764            ...where:
2765            e_c = eccentricity
2766            F_a = rough guess
2767            c_a = cosh(F_a) = 0.5 * [2 * M_h / e_c + e_c / (2 * M_h)],
2768            s_a = sinh(F_a) = 0.5 * [2 * M_h / e_c - e_c / (2 * M_h)]
2769
2770            Although the equation may look intimidating, there are a lot of repeated values.
2771            We can simplify the equation by extracting the repeated values.
2772
2773            Let:
2774                alpha = e_c^2 / (4 * M_h) + F_a
2775                beta  = 1 / (e_c * c_a - 1)
2776                gamma = alpha * beta
2777
2778            The equation gets simplified into:
2779
2780            delta = (
2781                    6 * gamma +
2782                    3 * e_c * s_a * beta * gamma^2
2783                ) / (
2784                    6 +
2785                    6 * e_c * s_a * beta * gamma +
2786                    e_c * c_a * beta * gamma^2
2787                )
2788
2789            Then we can refine the rough guess into the initial guess:
2790            F_0 = F_a + delta
2791            */
2792
2793            let (c_a, s_a) = {
2794                // c_a and s_a has a lot of repeated values, so we can
2795                // optimize by calculating them together.
2796                // c_a, s_a = 0.5 * [2 * M_h / e_c +- e_c / (2 * M_h)]
2797                //
2798                // define "left"  = 2 * M_h / e_c
2799                // define "right" = e_c / (2 * M_h)
2800
2801                let left = 2.0 * mean_anomaly / eccentricity;
2802                let right = eccentricity / (2.0 * mean_anomaly);
2803
2804                (0.5 * (left + right), 0.5 * (left - right))
2805            };
2806
2807            let alpha = eccentricity * eccentricity / (4.0 * mean_anomaly) + rough_guess;
2808
2809            let beta = (eccentricity * c_a - 1.0).recip();
2810
2811            let gamma = alpha * beta;
2812            let gamma_sq = gamma * gamma;
2813
2814            let delta = (6.0 * alpha * beta + 3.0 * (eccentricity * s_a * beta) * gamma_sq)
2815                / (6.0
2816                    + 6.0 * (eccentricity * s_a * beta) * gamma
2817                    + (eccentricity * c_a * beta) * gamma_sq);
2818
2819            rough_guess + delta
2820        }
2821    }
2822
2823    /// Gets the hyperbolic eccentric anomaly of the orbit.
2824    ///
2825    /// # Unchecked Operation
2826    /// This function does not check whether or not the orbit is actually hyperbolic.  
2827    /// Nonsensical output may be produced if the orbit is not hyperbolic, but rather
2828    /// elliptic or parabolic.
2829    ///
2830    /// # Performance
2831    /// This function uses numerical methods to approach the value and therefore
2832    /// is not performant. It is recommended to cache this value if you can.
2833    ///
2834    /// # Source
2835    /// From the paper:  
2836    /// "A new method for solving the hyperbolic Kepler equation"  
2837    /// by Baisheng Wu et al.  
2838    fn get_hyperbolic_eccentric_anomaly(&self, mean_anomaly: f64) -> f64 {
2839        let mut ecc_anom = self.get_approx_hyperbolic_eccentric_anomaly(mean_anomaly);
2840
2841        /*
2842        Do a fourth-order Schröder iteration of the second kind
2843
2844        Equation 25 of "A new method for solving the hyperbolic Kepler equation"
2845        by Baisheng Wu et al.
2846        Slightly restructured:
2847
2848        F_1^(4) = F_0 - (
2849            (6h/h' - 3h^2 h'' / h'^3) /
2850            (6 - 6h h'' / h'^2 + h^2 h'''/h'^3)
2851        )
2852
2853        ...where:
2854        e_c = eccentricity
2855        F_0 = initial guess
2856        h   = e_c sinh(F_0) - F_0 - M_h
2857        h'  = e_c cosh(F_0) - 1
2858        h'' = e_c sinh(F_0)
2859            = h + F_0 + M_h
2860        h'''= h' + 1
2861
2862        Rearranging for efficiency:
2863        h'''= e_c cosh(F_0)
2864        h'  = h''' - 1
2865        h'' = e_c sinh(F_0)
2866        h   = h'' - F_0 - M_h
2867
2868        Factoring out 1/h':
2869
2870        let r = 1 / h'
2871
2872        F_1^(4) = F_0 - (
2873            (6hr - 3h^2 h'' r^3) /
2874            (6 - 6h h'' r^2 + h^2 h''' r^3)
2875        )
2876
2877        Since sinh and cosh are very similar algebraically,
2878        it may be better to calculate them together.
2879
2880        Paper about Schröder iterations:
2881        https://doi.org/10.1016/j.cam.2019.02.035
2882         */
2883
2884        let eccentricity = self.get_eccentricity();
2885
2886        for _ in 0..NUMERIC_MAX_ITERS {
2887            let (sinh_eca, cosh_eca) = sinhcosh(ecc_anom);
2888
2889            let hppp = eccentricity * cosh_eca;
2890            let hp = hppp - 1.0;
2891            let hpp = eccentricity * sinh_eca;
2892            let h = hpp - ecc_anom - mean_anomaly;
2893
2894            let h_sq = h * h;
2895            let r = hp.recip();
2896            let r_sq = r * r;
2897            let r_cub = r_sq * r;
2898
2899            let denominator = 6.0 - 6.0 * h * hpp * r_sq + h_sq * hppp * r_cub;
2900
2901            if denominator.abs() < 1e-30 || !denominator.is_finite() {
2902                // dangerously close to div-by-zero, break out
2903                // eprintln!(
2904                //     "Hyperbolic eccentric anomaly solver: denominator is too small or not finite"
2905                // );
2906                break;
2907            }
2908
2909            let numerator = 6.0 * h * r - 3.0 * h_sq * hpp * r_cub;
2910            let delta = numerator / denominator;
2911
2912            ecc_anom -= delta;
2913
2914            if delta.abs() < 1e-12 {
2915                break;
2916            }
2917        }
2918
2919        ecc_anom
2920    }
2921
2922    /// Gets the elliptic eccentric anomaly of the orbit.
2923    ///
2924    /// # Unchecked Operation
2925    /// This function does not check whether or not the orbit is actually elliptic (e < 1).  
2926    /// Nonsensical output may be produced if the orbit is not elliptic, but rather
2927    /// hyperbolic or parabolic.
2928    ///
2929    /// # Performance
2930    /// This function uses numerical methods to approach the value and therefore
2931    /// is not performant. It is recommended to cache this value if you can.
2932    ///
2933    /// # Source
2934    /// From the paper  
2935    /// "An improved algorithm due to laguerre for the solution of Kepler's equation."  
2936    /// by Bruce A. Conway  
2937    /// <https://doi.org/10.1007/bf01230852>
2938    fn get_elliptic_eccentric_anomaly(&self, mut mean_anomaly: f64) -> f64 {
2939        let mut sign = 1.0;
2940        // Use the symmetry and periodicity of the eccentric anomaly
2941        // Equation 2 from the paper
2942        // "Two fast and accurate routines for solving
2943        // the elliptic Kepler equation for all values
2944        // of the eccentricity and mean anomaly"
2945        mean_anomaly %= TAU;
2946        if mean_anomaly > PI {
2947            // return self.get_eccentric_anomaly_elliptic(mean_anomaly - TAU);
2948            mean_anomaly -= TAU;
2949        }
2950        if mean_anomaly < 0.0 {
2951            // return -self.get_eccentric_anomaly_elliptic(-mean_anomaly);
2952            mean_anomaly = -mean_anomaly;
2953            sign = -1.0;
2954        }
2955
2956        // Starting guess
2957        // Section 2.1.2, 'The "rational seed"',
2958        // equation 19, from the paper
2959        // "Two fast and accurate routines for solving
2960        // the elliptic Kepler equation for all values
2961        // of the eccentricity and mean anomaly"
2962        //
2963        // E_0 = M + (4beM(pi - M)) / (8eM + 4e(e-pi) + pi^2)
2964        // where:
2965        // e = eccentricity
2966        // M = mean anomaly
2967        // pi = the constant PI
2968        // b = the constant B
2969        let eccentricity = self.get_eccentricity();
2970        let mut eccentric_anomaly = mean_anomaly
2971            + (4.0 * eccentricity * B * mean_anomaly * (PI - mean_anomaly))
2972                / (8.0 * eccentricity * mean_anomaly
2973                    + 4.0 * eccentricity * (eccentricity - PI)
2974                    + PI_SQUARED);
2975
2976        // Laguerre's method
2977        //
2978        // i = 2, 3, ..., n
2979        //
2980        // D = sqrt((n-1)^2(f'(x_i))^2 - n(n-1)f(x_i)f''(x_i))
2981        //
2982        // x_i+1 = x_i - (nf(x_i) / (f'(x_i) +/- D))
2983        // ...where the "+/-" is chosen to so that abs(denominator) is maximized
2984        for _ in 2..N_U32 {
2985            let f = keplers_equation(mean_anomaly, eccentric_anomaly, eccentricity);
2986            let fp = keplers_equation_derivative(eccentric_anomaly, eccentricity);
2987            let fpp = keplers_equation_second_derivative(eccentric_anomaly, eccentricity);
2988
2989            let n = N_F64;
2990            let n_minus_1 = n - 1.0;
2991            let d = ((n_minus_1 * n_minus_1) * fp * fp - n * n_minus_1 * f * fpp)
2992                .abs()
2993                .sqrt()
2994                .copysign(fp);
2995
2996            let denominator = n * f / (fp + d.max(1e-30));
2997            eccentric_anomaly -= denominator;
2998
2999            if denominator.abs() < 1e-30 || !denominator.is_finite() {
3000                // dangerously close to div-by-zero, break out
3001                break;
3002            }
3003        }
3004
3005        eccentric_anomaly * sign
3006    }
3007
3008    /// Gets the eccentric anomaly at a given true anomaly in the orbit.
3009    ///
3010    /// When the orbit is open (has an eccentricity of at least 1),
3011    /// the [hyperbolic eccentric anomaly](https://space.stackexchange.com/questions/27602/what-is-hyperbolic-eccentric-anomaly-f)
3012    /// would be returned instead.
3013    ///
3014    /// The eccentric anomaly is an angular parameter that defines the position
3015    /// of a body that is moving along an elliptic Kepler orbit.
3016    ///
3017    /// — [Wikipedia](https://en.wikipedia.org/wiki/Eccentric_anomaly)
3018    ///
3019    /// # Parabolic Support
3020    /// This function doesn't support parabolic trajectories yet.  
3021    /// `NaN`s or nonsensical values may be returned.
3022    ///
3023    /// # Performance
3024    /// The method to get the eccentric anomaly from the true anomaly
3025    /// uses a few trigonometry operations, and so it is not too performant.  
3026    /// It is, however, faster than the numerical approach methods used by
3027    /// the mean anomaly to eccentric anomaly conversion.  
3028    /// It is still recommended to cache this value if you can.
3029    #[doc(alias = "get_eccentric_anomaly_at_angle")]
3030    fn get_eccentric_anomaly_at_true_anomaly(&self, true_anomaly: f64) -> f64 {
3031        // TODO: PARABOLIC SUPPORT: This does not play well with parabolic trajectories.
3032        // Implement inverse of Barker's Equation for parabolas.
3033        let e = self.get_eccentricity();
3034        let true_anomaly = true_anomaly.rem_euclid(TAU);
3035
3036        if e < 1.0 {
3037            // let v = true_anomaly,
3038            //   e = eccentricity,
3039            //   E = eccentric anomaly
3040            //
3041            // https://en.wikipedia.org/wiki/True_anomaly#From_the_eccentric_anomaly:
3042            // tan(v / 2) = sqrt((1 + e)/(1 - e)) * tan(E / 2)
3043            // 1 = sqrt((1 + e)/(1 - e)) * tan(E / 2) / tan(v / 2)
3044            // 1 / tan(E / 2) = sqrt((1 + e)/(1 - e)) / tan(v / 2)
3045            // tan(E / 2) = tan(v / 2) / sqrt((1 + e)/(1 - e))
3046            // E / 2 = atan(tan(v / 2) / sqrt((1 + e)/(1 - e)))
3047            // E = 2 * atan(tan(v / 2) / sqrt((1 + e)/(1 - e)))
3048            // E = 2 * atan(tan(v / 2) * sqrt((1 - e)/(1 + e)))
3049
3050            2.0 * ((true_anomaly * 0.5).tan() * ((1.0 - e) / (1.0 + e)).sqrt()).atan()
3051        } else {
3052            // From the presentation "Spacecraft Dynamics and Control"
3053            // by Matthew M. Peet
3054            // https://control.asu.edu/Classes/MAE462/462Lecture05.pdf
3055            // Slide 25 of 27
3056            // Section "The Method for Hyperbolic Orbits"
3057            //
3058            // tan(f/2) = sqrt((e+1)/(e-1))*tanh(H/2)
3059            // 1 / tanh(H/2) = sqrt((e+1)/(e-1)) / tan(f/2)
3060            // tanh(H/2) = tan(f/2) / sqrt((e+1)/(e-1))
3061            // tanh(H/2) = tan(f/2) * sqrt((e-1)/(e+1))
3062            // H/2 = atanh(tan(f/2) * sqrt((e-1)/(e+1)))
3063            // H = 2 atanh(tan(f/2) * sqrt((e-1)/(e+1)))
3064            2.0 * ((true_anomaly * 0.5).tan() * ((e - 1.0) / (e + 1.0)).sqrt()).atanh()
3065        }
3066    }
3067
3068    /// Gets the eccentric anomaly at a given time in the orbit.
3069    ///
3070    /// When the orbit is open (has an eccentricity of at least 1),
3071    /// the [hyperbolic eccentric anomaly](https://space.stackexchange.com/questions/27602/what-is-hyperbolic-eccentric-anomaly-f)
3072    /// would be returned instead.
3073    ///
3074    /// The eccentric anomaly is an angular parameter that defines the position
3075    /// of a body that is moving along an elliptic Kepler orbit.
3076    ///
3077    /// — [Wikipedia](https://en.wikipedia.org/wiki/Eccentric_anomaly)
3078    ///
3079    /// # Time
3080    /// The time is expressed in seconds.
3081    ///
3082    /// # Performance
3083    /// The method to get the eccentric anomaly from the time
3084    /// uses numerical approach methods, and so it is not performant.  
3085    /// It is recommended to cache this value if you can.
3086    fn get_eccentric_anomaly_at_time(&self, time: f64) -> f64 {
3087        self.get_eccentric_anomaly_at_mean_anomaly(self.get_mean_anomaly_at_time(time))
3088    }
3089
3090    /// Gets the true anomaly at a given eccentric anomaly in the orbit.
3091    ///
3092    /// This function is faster than the function which takes mean anomaly as input,
3093    /// as the eccentric anomaly is hard to calculate.
3094    ///
3095    /// This function returns +/- pi for parabolic orbits due to how the equation works,
3096    /// and so **may result in infinities when combined with other functions**.
3097    ///
3098    /// The true anomaly is the angle between the direction of periapsis
3099    /// and the current position of the body, as seen from the main focus
3100    /// of the ellipse.
3101    ///
3102    /// — [Wikipedia](https://en.wikipedia.org/wiki/True_anomaly)
3103    ///
3104    /// # Performance  
3105    /// This function is faster than the function which takes mean anomaly as input,
3106    /// as the eccentric anomaly is hard to calculate.  
3107    /// However, this function still uses a few trigonometric functions, so it is
3108    /// not too performant.  
3109    /// It is recommended to cache this value if you can.
3110    fn get_true_anomaly_at_eccentric_anomaly(&self, eccentric_anomaly: f64) -> f64 {
3111        let eccentricity = self.get_eccentricity();
3112        if eccentricity < 1.0 {
3113            // https://en.wikipedia.org/wiki/True_anomaly#From_the_eccentric_anomaly
3114            let (s, c) = eccentric_anomaly.sin_cos();
3115            let beta = eccentricity / (1.0 + (1.0 - eccentricity * eccentricity).sqrt());
3116
3117            eccentric_anomaly + 2.0 * (beta * s / (1.0 - beta * c)).atan()
3118        } else {
3119            // From the presentation "Spacecraft Dynamics and Control"
3120            // by Matthew M. Peet
3121            // https://control.asu.edu/Classes/MAE462/462Lecture05.pdf
3122            // Slide 25 of 27
3123            // Section "The Method for Hyperbolic Orbits"
3124            //
3125            // tan(f/2) = sqrt((e+1)/(e-1))*tanh(H/2)
3126            // f/2 = atan(sqrt((e+1)/(e-1))*tanh(H/2))
3127            // f = 2atan(sqrt((e+1)/(e-1))*tanh(H/2))
3128
3129            2.0 * (((eccentricity + 1.0) / (eccentricity - 1.0)).sqrt()
3130                * (eccentric_anomaly * 0.5).tanh())
3131            .atan()
3132        }
3133    }
3134
3135    /// Gets the true anomaly at a given mean anomaly in the orbit.
3136    ///
3137    /// This function returns +/- pi for parabolic orbits due to how the equation works,
3138    /// and so **may result in infinities when combined with other functions**.
3139    ///
3140    /// The true anomaly is the angle between the direction of periapsis
3141    /// and the current position of the body, as seen from the main focus
3142    /// of the ellipse.
3143    ///
3144    /// — [Wikipedia](https://en.wikipedia.org/wiki/True_anomaly)
3145    ///
3146    /// # Performance
3147    /// The true anomaly is derived from the eccentric anomaly, which
3148    /// uses numerical approach methods and so is not performant.  
3149    /// It is recommended to cache this value if you can.
3150    ///
3151    /// Alternatively, if you already know the eccentric anomaly, you should use
3152    /// [`get_true_anomaly_at_eccentric_anomaly`][OrbitTrait::get_true_anomaly_at_eccentric_anomaly]
3153    /// instead.
3154    fn get_true_anomaly_at_mean_anomaly(&self, mean_anomaly: f64) -> f64 {
3155        self.get_true_anomaly_at_eccentric_anomaly(
3156            self.get_eccentric_anomaly_at_mean_anomaly(mean_anomaly),
3157        )
3158    }
3159
3160    /// Gets the true anomaly at a given time in the orbit.
3161    ///
3162    /// The true anomaly is the angle between the direction of periapsis
3163    /// and the current position of the body, as seen from the main focus
3164    /// of the ellipse.
3165    ///
3166    /// — [Wikipedia](https://en.wikipedia.org/wiki/True_anomaly)
3167    ///
3168    /// This function returns +/- pi for parabolic orbits due to how the equation works,
3169    /// and so **may result in infinities when combined with other functions**.
3170    ///
3171    /// # Time
3172    /// The time is expressed in seconds.
3173    ///
3174    /// # Performance
3175    /// The true anomaly is derived from the eccentric anomaly, which
3176    /// uses numerical approach methods and so is not performant.  
3177    /// It is recommended to cache this value if you can.
3178    ///
3179    /// Alternatively, if you already know the eccentric anomaly, you should use
3180    /// [`get_true_anomaly_at_eccentric_anomaly`][OrbitTrait::get_true_anomaly_at_eccentric_anomaly]
3181    /// instead.
3182    ///
3183    /// If you already know the mean anomaly, consider using
3184    /// [`get_true_anomaly_at_mean_anomaly`][OrbitTrait::get_true_anomaly_at_mean_anomaly]
3185    /// instead.  
3186    /// It won't help performance much, but it's not zero.
3187    fn get_true_anomaly_at_time(&self, time: f64) -> f64 {
3188        self.get_true_anomaly_at_mean_anomaly(self.get_mean_anomaly_at_time(time))
3189    }
3190
3191    /// Gets the mean anomaly at a given time in the orbit.
3192    ///
3193    /// The mean anomaly is the fraction of an elliptical orbit's period
3194    /// that has elapsed since the orbiting body passed periapsis,
3195    /// expressed as an angle which can be used in calculating the position
3196    /// of that body in the classical two-body problem.
3197    ///
3198    /// — [Wikipedia](https://en.wikipedia.org/wiki/Mean_anomaly)
3199    ///
3200    /// # Time
3201    /// The time is expressed in seconds.
3202    ///
3203    /// # Performance
3204    /// This function is performant and is unlikely to be the culprit of
3205    /// any performance issues.
3206    fn get_mean_anomaly_at_time(&self, time: f64) -> f64 {
3207        // M = t * sqrt(mu / |a^3|) + M_0
3208        time * (self.get_gravitational_parameter() / self.get_semi_major_axis().powi(3).abs())
3209            .sqrt()
3210            + self.get_mean_anomaly_at_epoch()
3211    }
3212
3213    /// Gets the mean anomaly at a given eccentric anomaly in the orbit.
3214    ///
3215    /// The mean anomaly is the fraction of an elliptical orbit's period
3216    /// that has elapsed since the orbiting body passed periapsis,
3217    /// expressed as an angle which can be used in calculating the position
3218    /// of that body in the classical two-body problem.
3219    ///
3220    /// — [Wikipedia](https://en.wikipedia.org/wiki/Mean_anomaly)
3221    ///
3222    /// # Parabolic Support
3223    /// This function doesn't consider parabolic trajectories yet.  
3224    /// `NaN`s or nonsensical values may be returned.
3225    ///
3226    /// # Performance
3227    /// This function is a wrapper around
3228    /// [`get_mean_anomaly_at_elliptic_eccentric_anomaly`][OrbitTrait::get_mean_anomaly_at_elliptic_eccentric_anomaly]
3229    /// and
3230    /// [`get_mean_anomaly_at_hyperbolic_eccentric_anomaly`][OrbitTrait::get_mean_anomaly_at_hyperbolic_eccentric_anomaly].  
3231    /// It does some trigonometry, but if you know `sin(eccentric_anomaly)` or `sinh(eccentric_anomaly)`
3232    /// beforehand, this can be skipped by directly using those inner functions.
3233    fn get_mean_anomaly_at_eccentric_anomaly(&self, eccentric_anomaly: f64) -> f64 {
3234        // TODO: PARABOLIC SUPPORT: This function doesn't consider parabolas yet.
3235        if self.get_eccentricity() < 1.0 {
3236            self.get_mean_anomaly_at_elliptic_eccentric_anomaly(
3237                eccentric_anomaly,
3238                eccentric_anomaly.sin(),
3239            )
3240        } else {
3241            self.get_mean_anomaly_at_hyperbolic_eccentric_anomaly(
3242                eccentric_anomaly,
3243                eccentric_anomaly.sinh(),
3244            )
3245        }
3246    }
3247
3248    /// Gets the mean anomaly at a given eccentric anomaly in the orbit and
3249    /// its precomputed sine.  
3250    ///
3251    /// The mean anomaly is the fraction of an elliptical orbit's period
3252    /// that has elapsed since the orbiting body passed periapsis,
3253    /// expressed as an angle which can be used in calculating the position
3254    /// of that body in the classical two-body problem.
3255    ///
3256    /// — [Wikipedia](https://en.wikipedia.org/wiki/Mean_anomaly)
3257    ///
3258    /// # Unchecked Operation
3259    /// This function does no checks on the validity of the value given
3260    /// as `sin_eccentric_anomaly`. It also doesn't check if the orbit is elliptic.  
3261    /// If invalid values are passed in, you will receive a possibly-nonsensical value as output.  
3262    ///
3263    /// # Performance
3264    /// This function is performant and is unlikely to be the culprit of
3265    /// any performance issues.
3266    fn get_mean_anomaly_at_elliptic_eccentric_anomaly(
3267        &self,
3268        eccentric_anomaly: f64,
3269        sin_eccentric_anomaly: f64,
3270    ) -> f64 {
3271        // https://en.wikipedia.org/wiki/Kepler%27s_equation#Equation
3272        //
3273        //      M = E - e sin E
3274        //
3275        // where:
3276        //   M = mean anomaly
3277        //   E = eccentric anomaly
3278        //   e = eccentricity
3279        eccentric_anomaly - self.get_eccentricity() * sin_eccentric_anomaly
3280    }
3281
3282    /// Gets the mean anomaly at a given eccentric anomaly in the orbit and
3283    /// its precomputed sine.  
3284    ///
3285    /// The mean anomaly is the fraction of an elliptical orbit's period
3286    /// that has elapsed since the orbiting body passed periapsis,
3287    /// expressed as an angle which can be used in calculating the position
3288    /// of that body in the classical two-body problem.
3289    ///
3290    /// — [Wikipedia](https://en.wikipedia.org/wiki/Mean_anomaly)
3291    ///
3292    /// # Unchecked Operation
3293    /// This function does no checks on the validity of the value given
3294    /// as `sinh_eccentric_anomaly`. It also doesn't check if the orbit is hyperbolic.  
3295    /// If invalid values are passed in, you will receive a possibly-nonsensical value as output.  
3296    ///
3297    /// # Performance
3298    /// This function is performant and is unlikely to be the culprit of
3299    /// any performance issues.
3300    fn get_mean_anomaly_at_hyperbolic_eccentric_anomaly(
3301        &self,
3302        eccentric_anomaly: f64,
3303        sinh_eccentric_anomaly: f64,
3304    ) -> f64 {
3305        // https://en.wikipedia.org/wiki/Kepler%27s_equation#Hyperbolic_Kepler_equation
3306        //
3307        //      M = e sinh(H) - H
3308        //
3309        // where:
3310        //   M = mean anomaly
3311        //   e = eccentricity
3312        //   H = hyperbolic eccentric anomaly
3313        self.get_eccentricity() * sinh_eccentric_anomaly - eccentric_anomaly
3314    }
3315
3316    /// Gets the mean anomaly at a given true anomaly in the orbit.
3317    ///
3318    /// The mean anomaly is the fraction of an elliptical orbit's period
3319    /// that has elapsed since the orbiting body passed periapsis,
3320    /// expressed as an angle which can be used in calculating the position
3321    /// of that body in the classical two-body problem.
3322    ///
3323    /// — [Wikipedia](https://en.wikipedia.org/wiki/Mean_anomaly)
3324    ///
3325    /// # Performance
3326    /// The method to get the eccentric anomaly from the true anomaly
3327    /// uses a few trigonometry operations, and so it is not too performant.  
3328    /// It is, however, faster than the numerical approach methods used by
3329    /// the mean anomaly to eccentric anomaly conversion.  
3330    /// It is still recommended to cache this value if you can.
3331    ///
3332    /// Alternatively, if you already know the eccentric anomaly, use
3333    /// [`get_mean_anomaly_at_eccentric_anomaly`][OrbitTrait::get_mean_anomaly_at_eccentric_anomaly]
3334    /// instead.
3335    #[doc(alias = "get_mean_anomaly_at_angle")]
3336    fn get_mean_anomaly_at_true_anomaly(&self, true_anomaly: f64) -> f64 {
3337        let ecc_anom = self.get_eccentric_anomaly_at_true_anomaly(true_anomaly);
3338        self.get_mean_anomaly_at_eccentric_anomaly(ecc_anom)
3339    }
3340
3341    /// Gets the 3D position at a given angle (true anomaly) in the orbit.
3342    ///
3343    /// # Angle
3344    /// The angle is expressed in radians, and ranges from 0 to tau.  
3345    /// Anything out of range will get wrapped around.
3346    ///
3347    /// # Performance
3348    /// This function is somewhat performant.
3349    ///
3350    /// This function benefits significantly from being in the
3351    /// [cached version of the orbit struct][crate::Orbit].  
3352    ///
3353    /// If you want to get both the position and velocity vectors, you can
3354    /// use the
3355    /// [`get_state_vectors_at_true_anomaly`][OrbitTrait::get_state_vectors_at_true_anomaly]
3356    /// function instead. It prevents redundant calculations and is therefore
3357    /// faster than calling the position and velocity functions separately.
3358    ///
3359    /// If you only want to get the altitude of the orbit, you can use the
3360    /// [`get_altitude_at_true_anomaly`][OrbitTrait::get_altitude_at_true_anomaly]
3361    /// function instead.
3362    ///
3363    /// If you already know the altitude at the angle, you can
3364    /// rotate the altitude using the true anomaly, then tilt
3365    /// it using the [`transform_pqw_vector`][OrbitTrait::transform_pqw_vector]
3366    /// function instead.
3367    ///
3368    /// # Example
3369    /// ```
3370    /// use glam::DVec3;
3371    ///
3372    /// use keplerian_sim::{Orbit, OrbitTrait};
3373    ///
3374    /// let mut orbit = Orbit::default();
3375    /// orbit.set_periapsis(100.0);
3376    /// orbit.set_eccentricity(0.0);
3377    ///
3378    /// let pos = orbit.get_position_at_true_anomaly(0.0);
3379    ///
3380    /// assert_eq!(pos, DVec3::new(100.0, 0.0, 0.0));
3381    /// ```
3382    #[doc(alias = "get_position_at_angle")]
3383    fn get_position_at_true_anomaly(&self, angle: f64) -> DVec3 {
3384        self.transform_pqw_vector(self.get_pqw_position_at_true_anomaly(angle))
3385    }
3386
3387    /// Gets the 3D position at a given eccentric anomaly in the orbit.
3388    ///
3389    /// # Performance
3390    /// This function benefits significantly from being in the
3391    /// [cached version of the orbit struct][crate::Orbit].  
3392    /// This function is not too performant as it uses a few trigonometric
3393    /// operations. It is recommended to cache this value if you can.
3394    ///
3395    /// Alternatively, if you already know the true anomaly, you can use the
3396    /// [`get_position_at_true_anomaly`][OrbitTrait::get_position_at_true_anomaly]
3397    /// function instead.  
3398    /// Or, if you only need the altitude, use the
3399    /// [`get_altitude_at_eccentric_anomaly`][OrbitTrait::get_altitude_at_eccentric_anomaly]
3400    /// function instead.
3401    ///
3402    /// If you want to get both the position and velocity vectors, you can
3403    /// use the
3404    /// [`get_state_vectors_at_eccentric_anomaly`][OrbitTrait::get_state_vectors_at_eccentric_anomaly]
3405    /// function instead. It prevents redundant calculations and is therefore
3406    /// faster than calling the position and velocity functions separately.
3407    fn get_position_at_eccentric_anomaly(&self, eccentric_anomaly: f64) -> DVec3 {
3408        self.transform_pqw_vector(self.get_pqw_position_at_eccentric_anomaly(eccentric_anomaly))
3409    }
3410
3411    /// Gets the speed at a given angle (true anomaly) in the orbit.
3412    ///
3413    /// The speed is derived from the vis-viva equation, and so is
3414    /// a lot faster than the velocity calculation.
3415    ///
3416    /// # Speed vs. Velocity
3417    /// Speed is not to be confused with velocity.  
3418    /// Speed tells you how fast something is moving,
3419    /// while velocity tells you how fast *and in what direction* it's moving in.
3420    ///
3421    /// # Angle
3422    /// The angle is expressed in radians, and ranges from 0 to tau.
3423    /// Anything out of range will get wrapped around.
3424    ///
3425    /// # Performance
3426    /// This function is performant, however, if you already know the altitude at the angle,
3427    /// you can use the [`get_speed_at_altitude`][OrbitTrait::get_speed_at_altitude]
3428    /// function instead to skip some calculations.
3429    ///
3430    /// # Example
3431    /// ```
3432    /// use keplerian_sim::{Orbit, OrbitTrait};
3433    ///
3434    /// let mut orbit = Orbit::default();
3435    /// orbit.set_periapsis(100.0);
3436    /// orbit.set_eccentricity(0.5);
3437    ///
3438    /// let speed_periapsis = orbit.get_speed_at_true_anomaly(0.0);
3439    /// let speed_apoapsis = orbit.get_speed_at_true_anomaly(std::f64::consts::PI);
3440    ///
3441    /// assert!(speed_periapsis > speed_apoapsis);
3442    /// ```
3443    #[doc(alias = "get_speed_at_angle")]
3444    fn get_speed_at_true_anomaly(&self, angle: f64) -> f64 {
3445        self.get_speed_at_altitude(self.get_altitude_at_true_anomaly(angle))
3446    }
3447
3448    /// Gets the speed at a given altitude in the orbit.
3449    ///
3450    /// The speed is derived from the vis-viva equation, and so is
3451    /// a lot faster than the velocity calculation.
3452    ///
3453    /// # Speed vs. Velocity
3454    /// Speed is not to be confused with velocity.  
3455    /// Speed tells you how fast something is moving,
3456    /// while velocity tells you how fast *and in what direction* it's moving in.
3457    ///
3458    /// # Unchecked Operation
3459    /// This function does no checks on the validity of the value given
3460    /// in the `altitude` parameter, namely whether or not this altitude
3461    /// is possible in the given orbit.  
3462    /// If invalid values are passed in, you will receive a possibly-nonsensical
3463    /// value as output.
3464    ///
3465    /// # Altitude
3466    /// The altitude is expressed in meters, and is the distance to the
3467    /// center of the orbit.
3468    ///
3469    /// # Performance
3470    /// This function is very performant and should not be the cause of any
3471    /// performance issues.
3472    ///
3473    /// # Example
3474    /// ```
3475    /// use keplerian_sim::{Orbit, OrbitTrait};
3476    ///
3477    /// const PERIAPSIS: f64 = 100.0;
3478    ///
3479    /// let mut orbit = Orbit::default();
3480    /// orbit.set_periapsis(PERIAPSIS);
3481    /// orbit.set_eccentricity(0.5);
3482    ///
3483    /// let apoapsis = orbit.get_apoapsis();
3484    ///
3485    /// let speed_periapsis = orbit.get_speed_at_altitude(PERIAPSIS);
3486    /// let speed_apoapsis = orbit.get_speed_at_altitude(apoapsis);
3487    ///
3488    /// assert!(speed_periapsis > speed_apoapsis);
3489    /// ```
3490    fn get_speed_at_altitude(&self, altitude: f64) -> f64 {
3491        // https://en.wikipedia.org/wiki/Vis-viva_equation
3492        // v^2 = GM (2/r - 1/a)
3493        // => v = sqrt(GM * (2/r - 1/a))
3494        //
3495        // note:
3496        // a = r_p / (1 - e)
3497        // => 1 / a = (1 - e) / r_p
3498
3499        let r = altitude;
3500        let a_recip = (1.0 - self.get_eccentricity()) / self.get_periapsis();
3501
3502        ((2.0 / r - a_recip) * self.get_gravitational_parameter()).sqrt()
3503    }
3504
3505    /// Gets the speed at the periapsis of the orbit.
3506    ///
3507    /// The speed is derived from the vis-viva equation, and so is
3508    /// a lot faster than the velocity calculation.
3509    ///
3510    /// # Speed vs. Velocity
3511    /// Speed is not to be confused with velocity.  
3512    /// Speed tells you how fast something is moving,
3513    /// while velocity tells you how fast *and in what direction* it's moving in.
3514    ///
3515    /// # Performance
3516    /// This function is very performant and should not be the cause of any
3517    /// performance issues.
3518    ///
3519    /// # Example
3520    /// ```
3521    /// use keplerian_sim::{Orbit, OrbitTrait};
3522    ///
3523    /// const PERIAPSIS: f64 = 100.0;
3524    ///
3525    /// let mut orbit = Orbit::default();
3526    /// orbit.set_periapsis(PERIAPSIS);
3527    /// orbit.set_eccentricity(0.5);
3528    ///
3529    /// let naive_getter = orbit.get_speed_at_altitude(PERIAPSIS);
3530    /// let dedicated_getter = orbit.get_speed_at_periapsis();
3531    ///
3532    /// assert!((naive_getter - dedicated_getter).abs() < 1e-14);
3533    /// ```
3534    fn get_speed_at_periapsis(&self) -> f64 {
3535        // https://en.wikipedia.org/wiki/Vis-viva_equation
3536        // v^2 = GM (2/r_p - 1/a)
3537        // => v = sqrt(GM * (2/r_p - 1/a))
3538        //
3539        // note:
3540        // a = r_p / (1 - e)
3541        // => 1 / a = (1 - e) / r_p
3542        //
3543        // => v = sqrt(
3544        //      GM
3545        //      * (2/r_p - (1 - e) / r_p)
3546        //    )
3547        // => v = sqrt(
3548        //      GM
3549        //      * ((2 - (1 - e)) / r_p)
3550        //    )
3551        //
3552        // note:
3553        // 2 - (1 - e) = e + 1
3554        //
3555        // => v = sqrt(
3556        //      GM
3557        //      * ((e + 1) / r_p)
3558        //    )
3559        (self.get_gravitational_parameter()
3560            * ((self.get_eccentricity() + 1.0) / self.get_periapsis()))
3561        .sqrt()
3562    }
3563
3564    /// Gets the speed at the apoapsis of the orbit.
3565    ///
3566    /// The speed is derived from the vis-viva equation, and so is
3567    /// a lot faster than the velocity calculation.
3568    ///
3569    /// # Speed vs. Velocity
3570    /// Speed is not to be confused with velocity.  
3571    /// Speed tells you how fast something is moving,
3572    /// while velocity tells you how fast *and in what direction* it's moving in.
3573    ///
3574    /// # Open orbits (eccentricity >= 1)
3575    /// This function does not handle open orbits specially, and will return
3576    /// a non-physical value. You might want to use the getter for the speed
3577    /// at infinity:
3578    /// [`get_speed_at_infinity`][OrbitTrait::get_speed_at_infinity]
3579    ///
3580    /// # Performance
3581    /// This function is very performant and should not be the cause of any
3582    /// performance issues.
3583    ///
3584    /// # Example
3585    /// ```
3586    /// use keplerian_sim::{Orbit, OrbitTrait};
3587    ///
3588    /// const APOAPSIS: f64 = 200.0;
3589    /// const PERIAPSIS: f64 = 100.0;
3590    ///
3591    /// let orbit = Orbit::new_flat_with_apoapsis(
3592    ///     APOAPSIS,
3593    ///     PERIAPSIS,
3594    ///     0.0, // Argument of periapsis
3595    ///     0.0, // Mean anomaly at epoch
3596    ///     1.0, // Gravitational parameter
3597    /// );
3598    ///
3599    /// let naive_getter = orbit.get_speed_at_altitude(APOAPSIS);
3600    /// let dedicated_getter = orbit.get_speed_at_apoapsis();
3601    ///
3602    /// assert!((naive_getter - dedicated_getter).abs() < 1e-14);
3603    /// ```
3604    fn get_speed_at_apoapsis(&self) -> f64 {
3605        // https://en.wikipedia.org/wiki/Vis-viva_equation
3606        // v^2 = GM (2/r_a - 1/a)
3607        // => v = sqrt(GM * (2/r_a - 1/a))
3608        //
3609        // note: to simplify,
3610        // we define `left` and `right`:
3611        // left := 2 / r_a; right := 1 / a
3612        //
3613        // and also define `inner`:
3614        // inner := left - right
3615        //
3616        // which means we can simplify v:
3617        // => v = sqrt(GM * inner)
3618        //
3619        // note:
3620        // a = r_p / (1 - e)
3621        // => 1 / a = (1 - e) / r_p
3622        //
3623        // => right = (1 - e) / r_p
3624        //
3625        // note:
3626        // r_a = a * (1 + e)
3627        // = r_p / (1 - e) * (1 + e)
3628        // = r_p * (1 + e) / (1 - e)
3629        //
3630        // => left = 2 / (r_p * (1 + e) / (1 - e))
3631        // = (2 * (1 - e)) / (r_p * (1 + e))
3632        //
3633        // note: use the same denominator `(r_p * (1 + e))`
3634        //
3635        // recall right = (1 - e) / r_p.
3636        // right = right * (1 + e)/(1 + e)
3637        // = ((1 - e) * (1 + e)) / (r_p * (1 + e))
3638        //
3639        // recall inner := left - right.
3640        // => inner = (2 * (1 - e)) / (r_p * (1 + e))
3641        //  - ((1 - e) * (1 + e)) / (r_p * (1 + e))
3642        //
3643        // factor out 1 / (r_p * (1 + e)):
3644        // => inner = (2 * (1 - e) - (1 - e) * (1 + e)) / (r_p * (1 + e))
3645        //
3646        // factor out (1 - e) in numerator:
3647        // => inner = ((2 - (1 + e)) * (1 - e)) / (r_p * (1 + e))
3648        // = ((2 - 1 - e) * (1 - e)) / (r_p * (1 + e))
3649        // = ((1 - e) * (1 - e)) / (r_p * (1 + e))
3650        // = (1 - e)^2 / (r_p * (1 + e))
3651        //
3652        // recall v = sqrt(GM * inner).
3653        // => v = sqrt(GM * (1 - e)^2 / (r_p * (1 + e)))
3654        //
3655        // recall that for all x >= 0, sqrt(x^2) = x,
3656        //   and that for all x < 0, sqrt(x^2) = -x.
3657        //
3658        // => we can factor out (1 - e)^2 outside the sqrt.
3659        // => v = |1 - e| sqrt(GM / (r_p * (1 + e)))
3660
3661        (1.0 - self.get_eccentricity()).abs()
3662            * (self.get_gravitational_parameter()
3663                / (self.get_periapsis() * (1.0 + self.get_eccentricity())))
3664            .sqrt()
3665    }
3666
3667    /// Gets the hyperbolic excess speed (v_∞) of the trajectory.
3668    ///
3669    /// Under simplistic assumptions a body traveling along
3670    /// [a hyperbolic] trajectory will coast towards infinity,
3671    /// settling to __a final excess velocity__ relative to
3672    /// the central body.
3673    ///
3674    /// \- [Wikipedia](https://en.wikipedia.org/wiki/Hyperbolic_trajectory)
3675    ///
3676    /// In other words, as the time of a hyperbolic trajectory
3677    /// approaches infinity, the speed approaches a certain
3678    /// speed, called the hyperbolic excess speed.
3679    ///
3680    /// # Unchecked Operation
3681    /// This function does not check that the orbit is open.  
3682    /// This function will return NaN for closed orbits (e < 1).
3683    ///
3684    /// # Speed vs. Velocity
3685    /// Speed is not to be confused with velocity.  
3686    /// Speed tells you how fast something is moving,
3687    /// while velocity tells you how fast *and in what direction* it's moving in.
3688    ///
3689    /// # Performance
3690    /// This function is very performant and should not be the cause of any
3691    /// performance issues.
3692    ///
3693    /// # Example
3694    /// ```
3695    /// use keplerian_sim::{Orbit, OrbitTrait};
3696    ///
3697    /// let orbit = Orbit::new_flat(
3698    ///     1.0, // Eccentricity
3699    ///     1.0, // Periapsis
3700    ///     0.0, // Argument of periapsis
3701    ///     0.0, // Mean anomaly at epoch
3702    ///     1.0, // Gravitational parameter
3703    /// );
3704    ///
3705    /// assert_eq!(orbit.get_speed_at_infinity(), 0.0);
3706    /// ```
3707    #[doc(alias = "get_hyperbolic_excess_speed")]
3708    #[doc(alias = "get_speed_at_asymptote")]
3709    fn get_speed_at_infinity(&self) -> f64 {
3710        // https://en.wikipedia.org/wiki/Hyperbolic_trajectory#Parameters_describing_a_hyperbolic_trajectory
3711        // v_∞ = sqrt(-μ / a)
3712        //
3713        // recall a = r_p / (1 - e)
3714        //
3715        // => v_∞ = sqrt(-μ / (r_p / (1 - e)))
3716        // => v_∞ = sqrt(-μ * (1 - e) / r_p)
3717        // => v_∞ = sqrt(μ * (e - 1) / r_p)
3718        (self.get_gravitational_parameter() * (self.get_eccentricity() - 1.0)
3719            / self.get_periapsis())
3720        .sqrt()
3721    }
3722
3723    /// Gets the speed at a given time in the orbit.
3724    ///
3725    /// # Time
3726    /// The time is expressed in seconds.
3727    ///
3728    /// # Performance
3729    /// The time will be converted into an eccentric anomaly, which uses
3730    /// numerical methods and so is not very performant.
3731    /// It is recommended to cache this value if you can.
3732    ///
3733    /// Alternatively, if you know the eccentric anomaly or the true anomaly,
3734    /// then you should use the
3735    /// [`get_speed_at_eccentric_anomaly`][OrbitTrait::get_speed_at_eccentric_anomaly]
3736    /// and
3737    /// [`get_speed_at_true_anomaly`][OrbitTrait::get_speed_at_true_anomaly]
3738    /// functions instead.  
3739    /// Those do not use numerical methods and therefore are a lot faster.
3740    ///
3741    /// # Speed vs. Velocity
3742    /// Speed is not to be confused with velocity.  
3743    /// Speed tells you how fast something is moving,
3744    /// while velocity tells you how fast *and in what direction* it's moving in.
3745    fn get_speed_at_time(&self, time: f64) -> f64 {
3746        self.get_speed_at_true_anomaly(self.get_true_anomaly_at_time(time))
3747    }
3748
3749    /// Gets the speed at a given eccentric anomaly in the orbit.
3750    ///
3751    /// The speed is derived from the vis-viva equation, and so is
3752    /// a lot faster than the velocity calculation.
3753    ///
3754    /// # Speed vs. Velocity
3755    /// Speed is not to be confused with velocity.  
3756    /// Speed tells you how fast something is moving,
3757    /// while velocity tells you how fast *and in what direction* it's moving in.
3758    ///
3759    /// # Performance
3760    /// This function is not too performant as it uses a few trigonometric
3761    /// operations.  
3762    /// It is recommended to cache this value if you can.  
3763    ///
3764    /// Alternatively, if you already know the true anomaly,
3765    /// then you should use the
3766    /// [`get_speed_at_true_anomaly`][OrbitTrait::get_speed_at_true_anomaly]
3767    /// function instead.
3768    fn get_speed_at_eccentric_anomaly(&self, eccentric_anomaly: f64) -> f64 {
3769        self.get_speed_at_true_anomaly(
3770            self.get_true_anomaly_at_eccentric_anomaly(eccentric_anomaly),
3771        )
3772    }
3773
3774    /// Gets the velocity at a given angle (true anomaly) in the orbit
3775    /// in the [perifocal coordinate system](https://en.wikipedia.org/wiki/Perifocal_coordinate_system).
3776    ///
3777    /// # Perifocal Coordinate System
3778    /// This function returns a vector in the perifocal coordinate (PQW) system, where
3779    /// the first element points to the periapsis, and the second element has a
3780    /// true anomaly 90 degrees past the periapsis. The third element points perpendicular
3781    /// to the orbital plane, and is always zero in this case, and so it is omitted.
3782    ///
3783    /// Learn more about the PQW system: <https://en.wikipedia.org/wiki/Perifocal_coordinate_system>
3784    ///
3785    /// If you want to get the vector in the regular coordinate system instead, use
3786    /// [`get_velocity_at_true_anomaly`][OrbitTrait::get_velocity_at_true_anomaly] instead.
3787    ///
3788    /// # Performance
3789    /// This function is not too performant as it uses some trigonometric operations.
3790    /// It is recommended to cache this value if you can.
3791    ///
3792    /// Alternatively, if you only want to know the speed, use
3793    /// [`get_speed_at_true_anomaly`][OrbitTrait::get_speed_at_true_anomaly] instead.  
3794    /// And if you already know the eccentric anomaly, use
3795    /// [`get_pqw_velocity_at_eccentric_anomaly`][OrbitTrait::get_pqw_velocity_at_eccentric_anomaly]
3796    /// instead.
3797    ///
3798    /// # Angle
3799    /// The angle is expressed in radians, and ranges from 0 to tau.  
3800    /// Anything out of range will get wrapped around.
3801    ///
3802    /// # Example
3803    /// ```
3804    /// use keplerian_sim::{Orbit, OrbitTrait};
3805    ///
3806    /// let mut orbit = Orbit::default();
3807    /// orbit.set_periapsis(100.0);
3808    /// orbit.set_eccentricity(0.5);
3809    ///
3810    /// let vel_periapsis = orbit.get_pqw_velocity_at_true_anomaly(0.0);
3811    /// let vel_apoapsis = orbit.get_pqw_velocity_at_true_anomaly(std::f64::consts::PI);
3812    ///
3813    /// let speed_periapsis = vel_periapsis.length();
3814    /// let speed_apoapsis = vel_apoapsis.length();
3815    ///
3816    /// assert!(speed_periapsis > speed_apoapsis);
3817    /// ```
3818    ///
3819    /// # Speed vs. Velocity
3820    /// Speed is not to be confused with velocity.  
3821    /// Speed tells you how fast something is moving,
3822    /// while velocity tells you how fast *and in what direction* it's moving in.
3823    #[doc(alias = "get_flat_velocity_at_angle")]
3824    #[doc(alias = "get_pqw_velocity_at_angle")]
3825    fn get_pqw_velocity_at_true_anomaly(&self, angle: f64) -> DVec2 {
3826        // TODO: PARABOLIC SUPPORT: This does not play well with parabolic trajectories.
3827        let outer_mult = (self.get_semi_major_axis() * self.get_gravitational_parameter())
3828            .abs()
3829            .sqrt()
3830            / self.get_altitude_at_true_anomaly(angle);
3831
3832        let q_mult = (1.0 - self.get_eccentricity().powi(2)).abs().sqrt();
3833
3834        let eccentric_anomaly = self.get_eccentric_anomaly_at_true_anomaly(angle);
3835
3836        let trig_ecc_anom = if self.get_eccentricity() < 1.0 {
3837            eccentric_anomaly.sin_cos()
3838        } else {
3839            sinhcosh(eccentric_anomaly)
3840        };
3841
3842        self.get_pqw_velocity_at_eccentric_anomaly_unchecked(outer_mult, q_mult, trig_ecc_anom)
3843    }
3844
3845    /// Gets the velocity at the periapsis of the orbit
3846    /// in the [perifocal coordinate system](https://en.wikipedia.org/wiki/Perifocal_coordinate_system).
3847    ///
3848    /// # Perifocal Coordinate System
3849    /// This function returns a vector in the perifocal coordinate (PQW) system, where
3850    /// the first element points to the periapsis, and the second element has a
3851    /// true anomaly 90 degrees past the periapsis. The third element points perpendicular
3852    /// to the orbital plane, and is always zero in this case, and so it is omitted.
3853    ///
3854    /// Learn more about the PQW system: <https://en.wikipedia.org/wiki/Perifocal_coordinate_system>
3855    ///
3856    /// If you want to get the vector in the regular coordinate system instead, use
3857    /// [`get_velocity_at_periapsis`][OrbitTrait::get_velocity_at_periapsis] instead.
3858    ///
3859    /// # Performance
3860    /// This function is very performant and should not be the cause of any
3861    /// performance issues.
3862    ///
3863    /// Alternatively, if you only want to know the speed, use
3864    /// [`get_speed_at_periapsis`][OrbitTrait::get_speed_at_periapsis] instead.  
3865    ///
3866    /// # Example
3867    /// ```
3868    /// use keplerian_sim::{Orbit, OrbitTrait};
3869    /// use glam::DVec2;
3870    ///
3871    /// let mut orbit = Orbit::default();
3872    /// orbit.set_periapsis(100.0);
3873    /// orbit.set_eccentricity(0.5);
3874    ///
3875    /// let vel = orbit.get_pqw_velocity_at_periapsis();
3876    ///
3877    /// assert_eq!(
3878    ///     vel,
3879    ///     DVec2::new(0.0, orbit.get_speed_at_periapsis())
3880    /// );
3881    /// ```
3882    ///
3883    /// # Speed vs. Velocity
3884    /// Speed is not to be confused with velocity.  
3885    /// Speed tells you how fast something is moving,
3886    /// while velocity tells you how fast *and in what direction* it's moving in.
3887    fn get_pqw_velocity_at_periapsis(&self) -> DVec2 {
3888        DVec2::new(0.0, self.get_speed_at_periapsis())
3889    }
3890
3891    /// Gets the velocity at the apoapsis of the orbit
3892    /// in the [perifocal coordinate system](https://en.wikipedia.org/wiki/Perifocal_coordinate_system).
3893    ///
3894    /// # Perifocal Coordinate System
3895    /// This function returns a vector in the perifocal coordinate (PQW) system, where
3896    /// the first element points to the periapsis, and the second element has a
3897    /// true anomaly 90 degrees past the periapsis. The third element points perpendicular
3898    /// to the orbital plane, and is always zero in this case, and so it is omitted.
3899    ///
3900    /// Learn more about the PQW system: <https://en.wikipedia.org/wiki/Perifocal_coordinate_system>
3901    ///
3902    /// If you want to get the vector in the regular coordinate system instead, use
3903    /// [`get_velocity_at_apoapsis`][OrbitTrait::get_velocity_at_apoapsis] instead.
3904    ///
3905    /// # Open orbits (eccentricity >= 1)
3906    /// This function does not handle open orbits specially, and will return
3907    /// a non-physical value. You might want to use the getters for the velocity
3908    /// at the incoming and outgoing asymptotes:
3909    /// - [`get_pqw_velocity_at_incoming_asymptote`][OrbitTrait::get_pqw_velocity_at_incoming_asymptote]
3910    /// - [`get_pqw_velocity_at_outgoing_asymptote`][OrbitTrait::get_pqw_velocity_at_outgoing_asymptote]
3911    ///
3912    /// # Performance
3913    /// This function is very performant and should not be the cause of any
3914    /// performance issues.
3915    ///
3916    /// Alternatively, if you only want to know the speed, use
3917    /// [`get_speed_at_apoapsis`][OrbitTrait::get_speed_at_apoapsis] instead.  
3918    ///
3919    /// # Example
3920    /// ```
3921    /// use keplerian_sim::{Orbit, OrbitTrait};
3922    /// use glam::DVec2;
3923    ///
3924    /// let mut orbit = Orbit::default();
3925    /// orbit.set_periapsis(100.0);
3926    /// orbit.set_eccentricity(0.5);
3927    ///
3928    /// let vel = orbit.get_pqw_velocity_at_apoapsis();
3929    ///
3930    /// assert_eq!(
3931    ///     vel,
3932    ///     DVec2::new(0.0, -orbit.get_speed_at_apoapsis())
3933    /// );
3934    /// ```
3935    ///
3936    /// # Speed vs. Velocity
3937    /// Speed is not to be confused with velocity.  
3938    /// Speed tells you how fast something is moving,
3939    /// while velocity tells you how fast *and in what direction* it's moving in.
3940    fn get_pqw_velocity_at_apoapsis(&self) -> DVec2 {
3941        DVec2::new(0.0, -self.get_speed_at_apoapsis())
3942    }
3943
3944    /// Gets the velocity at the incoming asymptote of the trajectory
3945    /// in the [perifocal coordinate system](https://en.wikipedia.org/wiki/Perifocal_coordinate_system).
3946    ///
3947    /// # Perifocal Coordinate System
3948    /// This function returns a vector in the perifocal coordinate (PQW) system, where
3949    /// the first element points to the periapsis, and the second element has a
3950    /// true anomaly 90 degrees past the periapsis. The third element points perpendicular
3951    /// to the orbital plane, and is always zero in this case, and so it is omitted.
3952    ///
3953    /// Learn more about the PQW system: <https://en.wikipedia.org/wiki/Perifocal_coordinate_system>
3954    ///
3955    /// If you want to get the vector in the regular coordinate system instead, use
3956    /// [`get_velocity_at_incoming_asymptote`][OrbitTrait::get_velocity_at_incoming_asymptote]
3957    /// instead.
3958    ///
3959    /// # Unchecked Operation
3960    /// This function does not check that the orbit is open.  
3961    /// This function will return a NaN vector for closed orbits (e < 1).
3962    ///
3963    /// # Performance
3964    /// This function is very performant and should not be the cause of any
3965    /// performance issues.
3966    ///
3967    /// Alternatively, if you only want to know the speed, use
3968    /// [`get_speed_at_infinity`][OrbitTrait::get_speed_at_infinity] instead.  
3969    ///
3970    /// # Example
3971    /// ```
3972    /// use keplerian_sim::{Orbit, OrbitTrait};
3973    /// use glam::DVec2;
3974    ///
3975    /// let mut orbit = Orbit::default();
3976    /// orbit.set_periapsis(100.0);
3977    /// orbit.set_eccentricity(1.5);
3978    ///
3979    /// let speed = orbit.get_speed_at_infinity();
3980    /// let vel = orbit.get_pqw_velocity_at_incoming_asymptote();
3981    ///
3982    /// assert!((vel.length() - speed).abs() < 1e-15);
3983    /// ```
3984    ///
3985    /// # Speed vs. Velocity
3986    /// Speed is not to be confused with velocity.  
3987    /// Speed tells you how fast something is moving,
3988    /// while velocity tells you how fast *and in what direction* it's moving in.
3989    fn get_pqw_velocity_at_incoming_asymptote(&self) -> DVec2 {
3990        let asymptote_true_anom = -self.get_true_anomaly_at_asymptote();
3991        let (sin_true, cos_true) = asymptote_true_anom.sin_cos();
3992        -self.get_speed_at_infinity() * DVec2::new(cos_true, sin_true)
3993    }
3994
3995    /// Gets the velocity at the outgoing asymptote of the trajectory
3996    /// in the [perifocal coordinate system](https://en.wikipedia.org/wiki/Perifocal_coordinate_system).
3997    ///
3998    /// # Perifocal Coordinate System
3999    /// This function returns a vector in the perifocal coordinate (PQW) system, where
4000    /// the first element points to the periapsis, and the second element has a
4001    /// true anomaly 90 degrees past the periapsis. The third element points perpendicular
4002    /// to the orbital plane, and is always zero in this case, and so it is omitted.
4003    ///
4004    /// Learn more about the PQW system: <https://en.wikipedia.org/wiki/Perifocal_coordinate_system>
4005    ///
4006    /// If you want to get the vector in the regular coordinate system instead, use
4007    /// [`get_velocity_at_outgoing_asymptote`][OrbitTrait::get_velocity_at_outgoing_asymptote]
4008    /// instead.
4009    ///
4010    /// # Unchecked Operation
4011    /// This function does not check that the orbit is open.  
4012    /// This function will return a NaN vector for closed orbits (e < 1).
4013    ///
4014    /// # Performance
4015    /// This function is very performant and should not be the cause of any
4016    /// performance issues.
4017    ///
4018    /// Alternatively, if you only want to know the speed, use
4019    /// [`get_speed_at_infinity`][OrbitTrait::get_speed_at_infinity] instead.  
4020    ///
4021    /// # Example
4022    /// ```
4023    /// use keplerian_sim::{Orbit, OrbitTrait};
4024    /// use glam::DVec2;
4025    ///
4026    /// let mut orbit = Orbit::default();
4027    /// orbit.set_periapsis(100.0);
4028    /// orbit.set_eccentricity(1.5);
4029    ///
4030    /// let speed = orbit.get_speed_at_infinity();
4031    /// let vel = orbit.get_pqw_velocity_at_outgoing_asymptote();
4032    ///
4033    /// assert!((vel.length() - speed).abs() < 1e-15);
4034    /// ```
4035    ///
4036    /// # Speed vs. Velocity
4037    /// Speed is not to be confused with velocity.  
4038    /// Speed tells you how fast something is moving,
4039    /// while velocity tells you how fast *and in what direction* it's moving in.
4040    #[doc(alias = "get_hyperbolic_excess_pqw_velocity")]
4041    fn get_pqw_velocity_at_outgoing_asymptote(&self) -> DVec2 {
4042        let asymptote_true_anom = self.get_true_anomaly_at_asymptote();
4043        let (sin_true, cos_true) = asymptote_true_anom.sin_cos();
4044        self.get_speed_at_infinity() * DVec2::new(cos_true, sin_true)
4045    }
4046
4047    /// Gets the velocity at a given eccentric anomaly in the orbit
4048    /// in the [perifocal coordinate system](https://en.wikipedia.org/wiki/Perifocal_coordinate_system).
4049    ///
4050    /// # Perifocal Coordinate System
4051    /// This function returns a vector in the perifocal coordinate (PQW) system, where
4052    /// the first element points to the periapsis, and the second element has a
4053    /// true anomaly 90 degrees past the periapsis. The third element points perpendicular
4054    /// to the orbital plane, and is always zero in this case, and so it is omitted.
4055    ///
4056    /// Learn more about the PQW system: <https://en.wikipedia.org/wiki/Perifocal_coordinate_system>
4057    ///
4058    /// If you want to get the vector in the regular coordinate system instead, use
4059    /// [`get_velocity_at_eccentric_anomaly`][OrbitTrait::get_velocity_at_eccentric_anomaly] instead.
4060    ///
4061    /// # Speed vs. Velocity
4062    /// Speed is not to be confused with velocity.  
4063    /// Speed tells you how fast something is moving,
4064    /// while velocity tells you how fast *and in what direction* it's moving in.
4065    ///
4066    /// # Parabolic Support
4067    /// This function doesn't consider parabolic trajectories yet.  
4068    /// `NaN`s or parabolic trajectories may be returned.
4069    ///
4070    /// # Performance
4071    /// This function is not too performant as it uses some trigonometric
4072    /// operations.  
4073    /// It is recommended to cache this value if you can.
4074    /// If you want to just get the speed, consider using the
4075    /// [`get_speed_at_eccentric_anomaly`][OrbitTrait::get_speed_at_eccentric_anomaly]
4076    /// function instead.
4077    ///
4078    /// Alternatively, if you already know some values (such as the altitude), consider
4079    /// using the unchecked version of the function instead:  
4080    /// [`get_pqw_velocity_at_eccentric_anomaly_unchecked`][OrbitTrait::get_pqw_velocity_at_eccentric_anomaly_unchecked]
4081    #[doc(alias = "get_flat_velocity_at_eccentric_anomaly")]
4082    fn get_pqw_velocity_at_eccentric_anomaly(&self, eccentric_anomaly: f64) -> DVec2 {
4083        // TODO: PARABOLIC SUPPORT: This does not play well with parabolic trajectories.
4084        let outer_mult = (self.get_semi_major_axis() * self.get_gravitational_parameter())
4085            .abs()
4086            .sqrt()
4087            / self.get_altitude_at_eccentric_anomaly(eccentric_anomaly);
4088
4089        let q_mult = (1.0 - self.get_eccentricity().powi(2)).abs().sqrt();
4090
4091        let trig_ecc_anom = if self.get_eccentricity() < 1.0 {
4092            eccentric_anomaly.sin_cos()
4093        } else {
4094            sinhcosh(eccentric_anomaly)
4095        };
4096
4097        self.get_pqw_velocity_at_eccentric_anomaly_unchecked(outer_mult, q_mult, trig_ecc_anom)
4098    }
4099
4100    /// Gets the velocity at a given eccentric anomaly in the orbit
4101    /// in the [perifocal coordinate system](https://en.wikipedia.org/wiki/Perifocal_coordinate_system).
4102    ///
4103    /// # Unchecked Operation
4104    /// This function does not check the validity of the
4105    /// inputs passed to this function.  
4106    /// It is your responsibility to make sure the inputs passed in are valid.  
4107    /// Failing to do so may result in nonsensical outputs.
4108    ///
4109    /// # Parameters
4110    /// ## `outer_mult`
4111    /// This parameter is a multiplier for the entire 2D vector.  
4112    /// If the orbit is elliptic (e < 1), it should be calculated by
4113    /// the formula `sqrt(GM * a) / r`, where `GM` is the gravitational
4114    /// parameter, `a` is the semi-major axis, and `r` is the altitude of
4115    /// the orbit at that point.  
4116    /// If the orbit is hyperbolic (e > 1), it should instead be calculated by
4117    /// the formula `sqrt(-GM * a) / r`.  
4118    /// For the general case, the formula `sqrt(abs(GM * a)) / r` can be used instead.
4119    ///
4120    /// ## `q_mult`
4121    /// This parameter is a multiplier for the second element in the PQW vector.  
4122    /// For elliptic orbits, it should be calculated by the formula `sqrt(1 - e^2)`,
4123    /// where `e` is the eccentricity of the orbit.  
4124    /// For hyperbolic orbits, it should be calculated by the formula `sqrt(e^2 - 1)`,
4125    /// where `e` is the eccentricity of the orbit.  
4126    /// Alternatively, for the general case, you can use the formula `sqrt(abs(1 - e^2))`.
4127    ///
4128    /// ## `trig_ecc_anom`
4129    /// **For elliptic orbits**, this parameter should be a tuple containing the sine and cosine
4130    /// values of the eccentric anomaly, respectively.  
4131    /// **For hyperbolic orbits**, this parameter should be a tuple containing the **hyperbolic**
4132    /// sine and **hyperbolic** cosine values of the eccentric anomaly, respectively.
4133    ///
4134    /// # Perifocal Coordinate System
4135    /// This function returns a vector in the perifocal coordinate (PQW) system, where
4136    /// the first element points to the periapsis, and the second element has a
4137    /// true anomaly 90 degrees past the periapsis. The third element points perpendicular
4138    /// to the orbital plane, and is always zero in this case, and so it is omitted.
4139    ///
4140    /// Learn more about the PQW system: <https://en.wikipedia.org/wiki/Perifocal_coordinate_system>
4141    ///
4142    /// You can convert from the PQW system to the regular 3D space using
4143    /// [`transform_pqw_vector`][OrbitTrait::transform_pqw_vector].
4144    ///
4145    /// # Speed vs. Velocity
4146    /// Speed is not to be confused with velocity.  
4147    /// Speed tells you how fast something is moving,
4148    /// while velocity tells you how fast *and in what direction* it's moving in.
4149    ///
4150    /// # Parabolic Support
4151    /// This function doesn't consider parabolic trajectories yet.  
4152    /// `NaN`s or parabolic trajectories may be returned.
4153    ///
4154    /// # Performance
4155    /// This function is very performant and should not be the cause of any
4156    /// performance issues.
4157    ///
4158    /// # Example
4159    /// ```
4160    /// use keplerian_sim::{Orbit, OrbitTrait, sinhcosh};
4161    ///
4162    /// # fn main() {
4163    /// let orbit = Orbit::default();
4164    ///
4165    /// let eccentric_anomaly: f64 = 1.25;
4166    ///
4167    /// let pqw_vel = orbit.get_pqw_velocity_at_eccentric_anomaly(eccentric_anomaly);
4168    ///
4169    /// let gm = orbit.get_gravitational_parameter();
4170    /// let a = orbit.get_semi_major_axis();
4171    /// let altitude = orbit.get_altitude_at_eccentric_anomaly(eccentric_anomaly);
4172    /// let outer_mult = (gm * a).sqrt() / altitude;
4173    ///
4174    /// let q_mult = (1.0 - orbit.get_eccentricity().powi(2)).sqrt();
4175    ///
4176    /// let trig_ecc_anom = eccentric_anomaly.sin_cos();
4177    ///
4178    /// let pqw_vel_2 = orbit
4179    ///     .get_pqw_velocity_at_eccentric_anomaly_unchecked(outer_mult, q_mult, trig_ecc_anom);
4180    ///
4181    /// assert_eq!(pqw_vel, pqw_vel_2);
4182    /// }
4183    /// ```
4184    /// ```
4185    /// use keplerian_sim::{Orbit, OrbitTrait, sinhcosh};
4186    ///
4187    /// # fn main() {
4188    /// let mut hyperbolic = Orbit::default();
4189    /// hyperbolic.set_eccentricity(3.0);
4190    ///
4191    /// let eccentric_anomaly: f64 = 2.35;
4192    ///
4193    /// let pqw_vel = hyperbolic.get_pqw_velocity_at_eccentric_anomaly(eccentric_anomaly);
4194    ///
4195    /// let gm = hyperbolic.get_gravitational_parameter();
4196    /// let a = hyperbolic.get_semi_major_axis();
4197    /// let altitude = hyperbolic.get_altitude_at_eccentric_anomaly(eccentric_anomaly);
4198    /// let outer_mult = (-gm * a).sqrt() / altitude;
4199    ///
4200    /// let q_mult = (hyperbolic.get_eccentricity().powi(2) - 1.0).sqrt();
4201    ///
4202    /// let trig_ecc_anom = sinhcosh(eccentric_anomaly);
4203    ///
4204    /// let pqw_vel_2 = hyperbolic
4205    ///     .get_pqw_velocity_at_eccentric_anomaly_unchecked(outer_mult, q_mult, trig_ecc_anom);
4206    ///
4207    /// assert_eq!(pqw_vel, pqw_vel_2);
4208    /// # }
4209    /// ```
4210    fn get_pqw_velocity_at_eccentric_anomaly_unchecked(
4211        &self,
4212        outer_mult: f64,
4213        q_mult: f64,
4214        trig_ecc_anom: (f64, f64),
4215    ) -> DVec2 {
4216        // ==== ELLIPTIC CASE: ====
4217        // https://downloads.rene-schwarz.com/download/M001-Keplerian_Orbit_Elements_to_Cartesian_State_Vectors.pdf
4218        // Equation 8:
4219        //                                   [      -sin E       ]
4220        // vector_o'(t) = sqrt(GM * a) / r * [ sqrt(1-e^2) cos E ]
4221        //                                   [         0         ]
4222        //
4223        // outer_mult = sqrt(GM * a) / r
4224        // trig_ecc_anom = eccentric_anomaly.sin_cos()
4225        // q_mult = sqrt(1 - e^2)
4226        //
4227        //
4228        // ==== HYPERBOLIC CASE: ====
4229        // https://space.stackexchange.com/a/54418
4230        //                                    [      -sinh F       ]
4231        // vector_o'(t) = sqrt(-GM * a) / r * [ sqrt(e^2-1) cosh F ]
4232        //                                    [         0          ]
4233        //
4234        // outer_mult = sqrt(GM * a) / r
4235        // trig_ecc_anom = (eccentric_anomaly.sinh(), eccentric_anomaly.cosh())
4236        // q_mult = sqrt(e^2 - 1)
4237        DVec2::new(
4238            outer_mult * -trig_ecc_anom.0,
4239            outer_mult * q_mult * trig_ecc_anom.1,
4240        )
4241    }
4242
4243    /// Gets the velocity at a given time in the orbit
4244    /// in the [perifocal coordinate system](https://en.wikipedia.org/wiki/Perifocal_coordinate_system).
4245    ///
4246    /// # Perifocal Coordinate System
4247    /// This function returns a vector in the perifocal coordinate (PQW) system, where
4248    /// the first element points to the periapsis, and the second element has a
4249    /// true anomaly 90 degrees past the periapsis. The third element points perpendicular
4250    /// to the orbital plane, and is always zero in this case, and so it is omitted.
4251    ///
4252    /// Learn more about the PQW system: <https://en.wikipedia.org/wiki/Perifocal_coordinate_system>
4253    ///
4254    /// If you want to get the vector in the regular coordinate system instead, use
4255    /// [`get_velocity_at_time`][OrbitTrait::get_velocity_at_time] instead.
4256    ///
4257    /// # Speed vs. Velocity
4258    /// Speed is not to be confused with velocity.  
4259    /// Speed tells you how fast something is moving,
4260    /// while velocity tells you how fast *and in what direction* it's moving in.
4261    ///
4262    /// # Time
4263    /// The time is expressed in seconds.
4264    ///
4265    /// # Performance
4266    /// This method involves converting the time into an eccentric anomaly,
4267    /// which uses numerical methods and so is not performant.  
4268    /// It is recommended to cache this value if you can.
4269    ///
4270    /// Alternatively, if you already know the eccentric anomaly or the true anomaly,
4271    /// then you should use the
4272    /// [`get_pqw_velocity_at_eccentric_anomaly`][OrbitTrait::get_pqw_velocity_at_eccentric_anomaly]
4273    /// and
4274    /// [`get_pqw_velocity_at_true_anomaly`][OrbitTrait::get_pqw_velocity_at_true_anomaly]
4275    /// functions instead.  
4276    /// Those do not use numerical methods and therefore are a lot faster.
4277    #[doc(alias = "get_flat_velocity_at_time")]
4278    fn get_pqw_velocity_at_time(&self, time: f64) -> DVec2 {
4279        self.get_pqw_velocity_at_eccentric_anomaly(self.get_eccentric_anomaly_at_time(time))
4280    }
4281
4282    /// Gets the position at a given angle (true anomaly) in the orbit
4283    /// in the [perifocal coordinate system](https://en.wikipedia.org/wiki/Perifocal_coordinate_system).
4284    ///
4285    /// # Perifocal Coordinate System
4286    /// This function returns a vector in the perifocal coordinate (PQW) system, where
4287    /// the first element points to the periapsis, and the second element has a
4288    /// true anomaly 90 degrees past the periapsis. The third element points perpendicular
4289    /// to the orbital plane, and is always zero in this case, and so it is omitted.
4290    ///
4291    /// Learn more about the PQW system: <https://en.wikipedia.org/wiki/Perifocal_coordinate_system>
4292    ///
4293    /// If you want to get the vector in the regular coordinate system instead, use
4294    /// [`get_position_at_true_anomaly`][OrbitTrait::get_position_at_true_anomaly] instead.
4295    ///
4296    /// # Angle
4297    /// The angle is expressed in radians, and ranges from 0 to tau.  
4298    /// Anything out of range will get wrapped around.
4299    ///
4300    /// # Performance
4301    /// This function is somewhat performant. However, if you already know
4302    /// the altitude beforehand, you might be interested in the unchecked
4303    /// version of this function:
4304    /// [`get_pqw_position_at_true_anomaly_unchecked`][OrbitTrait::get_pqw_position_at_true_anomaly_unchecked]  
4305    /// If you're looking to just get the altitude at a given angle,
4306    /// consider using the [`get_altitude_at_true_anomaly`][OrbitTrait::get_altitude_at_true_anomaly]
4307    /// function instead.
4308    ///
4309    /// # Example
4310    /// ```
4311    /// use glam::DVec2;
4312    /// use keplerian_sim::{Orbit, OrbitTrait};
4313    ///
4314    /// let mut orbit = Orbit::default();
4315    /// orbit.set_periapsis(100.0);
4316    /// orbit.set_eccentricity(0.0);
4317    ///
4318    /// let pos = orbit.get_pqw_position_at_true_anomaly(0.0);
4319    ///
4320    /// assert_eq!(pos, DVec2::new(100.0, 0.0));
4321    /// ```
4322    #[doc(alias = "get_flat_position_at_angle")]
4323    #[doc(alias = "get_pqw_position_at_angle")]
4324    fn get_pqw_position_at_true_anomaly(&self, angle: f64) -> DVec2 {
4325        let alt = self.get_altitude_at_true_anomaly(angle);
4326        let (sin, cos) = angle.sin_cos();
4327        DVec2::new(alt * cos, alt * sin)
4328    }
4329
4330    /// Gets the position at a certain point in the orbit
4331    /// in the [perifocal coordinate system](https://en.wikipedia.org/wiki/Perifocal_coordinate_system).
4332    ///
4333    /// # Unchecked Operation
4334    /// This function does not check on the validity of the parameters.  
4335    /// Invalid values may lead to nonsensical results.
4336    ///
4337    /// # Parameters
4338    /// ## `altitude`
4339    /// The altitude at that certain point in the orbit.  
4340    /// The altitude is measured in meters, and measured from the
4341    /// center of the parent body (origin).
4342    /// ## `sincos_angle`
4343    /// A tuple containing the sine and cosine (respectively) of the true anomaly
4344    /// of the point in the orbit.
4345    ///
4346    /// # Perifocal Coordinate System
4347    /// This function returns a vector in the perifocal coordinate (PQW) system, where
4348    /// the first element points to the periapsis, and the second element has a
4349    /// true anomaly 90 degrees past the periapsis. The third element points perpendicular
4350    /// to the orbital plane, and is always zero in this case, and so it is omitted.
4351    ///
4352    /// Learn more about the PQW system: <https://en.wikipedia.org/wiki/Perifocal_coordinate_system>
4353    ///
4354    /// To convert from the PQW coordinates to regular 3D space, use the
4355    /// [`transform_pqw_vector`][OrbitTrait::transform_pqw_vector] function.
4356    ///
4357    /// # Angle
4358    /// The angle is expressed in radians, and ranges from 0 to tau.  
4359    /// Anything out of range will get wrapped around.
4360    ///
4361    /// # Performance
4362    /// This function is very performant and should not be the cause of any performance
4363    /// issues.
4364    ///
4365    /// # Example
4366    /// ```
4367    /// use glam::DVec2;
4368    /// use keplerian_sim::{Orbit, OrbitTrait};
4369    ///
4370    /// let mut orbit = Orbit::default();
4371    /// orbit.set_periapsis(100.0);
4372    /// orbit.set_eccentricity(0.0);
4373    ///
4374    /// let true_anomaly = 1.06;
4375    ///
4376    /// let pos = orbit.get_pqw_position_at_true_anomaly(true_anomaly);
4377    ///
4378    /// let altitude = orbit.get_altitude_at_true_anomaly(true_anomaly);
4379    /// let sincos_angle = true_anomaly.sin_cos();
4380    ///
4381    /// let pos2 = orbit.get_pqw_position_at_true_anomaly_unchecked(altitude, sincos_angle);
4382    ///
4383    /// assert_eq!(pos, pos2);
4384    /// ```
4385    fn get_pqw_position_at_true_anomaly_unchecked(
4386        &self,
4387        altitude: f64,
4388        sincos_angle: (f64, f64),
4389    ) -> DVec2 {
4390        DVec2::new(altitude * sincos_angle.1, altitude * sincos_angle.0)
4391    }
4392
4393    // TODO: DOC: POST-PARABOLIC SUPPORT: Update doc
4394    /// Gets the position at a given time in the orbit
4395    /// in the [perifocal coordinate system](https://en.wikipedia.org/wiki/Perifocal_coordinate_system).
4396    ///
4397    /// # Perifocal Coordinate System
4398    /// This function returns a vector in the perifocal coordinate (PQW) system, where
4399    /// the first element points to the periapsis, and the second element has a
4400    /// true anomaly 90 degrees past the periapsis. The third element points perpendicular
4401    /// to the orbital plane, and is always zero in this case, and so it is omitted.
4402    ///
4403    /// Learn more about the PQW system: <https://en.wikipedia.org/wiki/Perifocal_coordinate_system>
4404    ///
4405    /// If you want to get the vector in the regular coordinate system instead, use
4406    /// [`get_position_at_time`][OrbitTrait::get_position_at_time] instead.
4407    ///
4408    /// # Time
4409    /// The time is expressed in seconds.
4410    ///
4411    /// # Parabolic Support
4412    /// **This function returns non-finite numbers for parabolic orbits**
4413    /// due to how the equation for true anomaly works.
4414    ///
4415    /// # Performance
4416    /// This involves calculating the true anomaly at a given time,
4417    /// and so is not performant.
4418    /// It is recommended to cache this value when possible.
4419    ///
4420    /// Alternatively, if you already know the eccentric anomaly or the true anomaly,
4421    /// consider using the
4422    /// [`get_pqw_position_at_eccentric_anomaly`][OrbitTrait::get_pqw_position_at_eccentric_anomaly]
4423    /// and
4424    /// [`get_pqw_position_at_true_anomaly`][OrbitTrait::get_pqw_position_at_true_anomaly]
4425    /// functions instead.
4426    /// Those do not use numerical methods and therefore are a lot faster.
4427    ///
4428    /// If you only want to get the altitude of the orbit, you can use the
4429    /// [`get_altitude_at_time`][OrbitTrait::get_altitude_at_time]
4430    /// function instead.
4431    #[doc(alias = "get_flat_position_at_time")]
4432    fn get_pqw_position_at_time(&self, time: f64) -> DVec2 {
4433        self.get_pqw_position_at_true_anomaly(self.get_true_anomaly_at_time(time))
4434    }
4435
4436    // TODO: DOC: POST-PARABOLIC SUPPORT: Update doc
4437    /// Gets the position at a given eccentric anomaly in the orbit
4438    /// in the [perifocal coordinate system](https://en.wikipedia.org/wiki/Perifocal_coordinate_system).
4439    ///
4440    /// # Perifocal Coordinate System
4441    /// This function returns a vector in the perifocal coordinate (PQW) system, where
4442    /// the first element points to the periapsis, and the second element has a
4443    /// true anomaly 90 degrees past the periapsis. The third element points perpendicular
4444    /// to the orbital plane, and is always zero in this case, and so it is omitted.
4445    ///
4446    /// Learn more about the PQW system: <https://en.wikipedia.org/wiki/Perifocal_coordinate_system>
4447    ///
4448    /// If you want to get the vector in the regular coordinate system instead, use
4449    /// [`get_position_at_eccentric_anomaly`][OrbitTrait::get_position_at_eccentric_anomaly] instead.
4450    ///
4451    /// # Time
4452    /// The time is expressed in seconds.
4453    ///
4454    /// # Parabolic Support
4455    /// **This function returns non-finite numbers for parabolic orbits**
4456    /// due to how the equation for true anomaly works.
4457    ///
4458    /// # Performance
4459    /// This function is not too performant as it uses a few trigonometric
4460    /// operations.
4461    /// It is recommended to cache this value if you can.
4462    ///
4463    /// Alternatively, if you already know the true anomaly,
4464    /// consider using the
4465    /// [`get_pqw_position_at_true_anomaly`][OrbitTrait::get_pqw_position_at_true_anomaly]
4466    /// function instead.
4467    #[doc(alias = "get_flat_position_at_eccentric_anomaly")]
4468    fn get_pqw_position_at_eccentric_anomaly(&self, eccentric_anomaly: f64) -> DVec2 {
4469        self.get_pqw_position_at_true_anomaly(
4470            self.get_true_anomaly_at_eccentric_anomaly(eccentric_anomaly),
4471        )
4472    }
4473
4474    /// Gets the velocity at a given angle (true anomaly) in the orbit.
4475    ///
4476    /// # Performance
4477    /// This function is not too performant as it uses some trigonometric operations.
4478    /// It is recommended to cache this value if you can.
4479    ///
4480    /// Alternatively, if you only want to know the speed, use
4481    /// [`get_speed_at_true_anomaly`][OrbitTrait::get_speed_at_true_anomaly] instead.  
4482    /// Or, if you already have the eccentric anomaly, use
4483    /// [`get_velocity_at_eccentric_anomaly`][OrbitTrait::get_velocity_at_eccentric_anomaly]
4484    /// instead.
4485    /// These functions do less work and therefore are a lot faster.
4486    ///
4487    /// If you want to get both the position and velocity vectors, you can
4488    /// use the
4489    /// [`get_state_vectors_at_true_anomaly`][OrbitTrait::get_state_vectors_at_true_anomaly]
4490    /// function instead. It prevents redundant calculations and is therefore
4491    /// faster than calling the position and velocity functions separately.
4492    ///
4493    /// # Angle
4494    /// The angle is expressed in radians, and ranges from 0 to tau.  
4495    /// Anything out of range will get wrapped around.
4496    ///
4497    /// # Example
4498    /// ```
4499    /// use keplerian_sim::{Orbit, OrbitTrait};
4500    ///
4501    /// let mut orbit = Orbit::default();
4502    /// orbit.set_periapsis(100.0);
4503    /// orbit.set_eccentricity(0.5);
4504    ///
4505    /// let vel_periapsis = orbit.get_velocity_at_true_anomaly(0.0);
4506    /// let vel_apoapsis = orbit.get_velocity_at_true_anomaly(std::f64::consts::PI);
4507    ///
4508    /// let speed_periapsis = vel_periapsis.length();
4509    /// let speed_apoapsis = vel_apoapsis.length();
4510    ///
4511    /// assert!(speed_periapsis > speed_apoapsis)
4512    /// ```
4513    ///
4514    /// # Speed vs. Velocity
4515    /// Speed is not to be confused with velocity.  
4516    /// Speed tells you how fast something is moving,
4517    /// while velocity tells you how fast *and in what direction* it's moving in.
4518    #[doc(alias = "get_velocity_at_angle")]
4519    fn get_velocity_at_true_anomaly(&self, angle: f64) -> DVec3 {
4520        self.transform_pqw_vector(self.get_pqw_velocity_at_true_anomaly(angle))
4521    }
4522
4523    /// Gets the velocity at the periapsis of the orbit.
4524    ///
4525    /// # Performance
4526    /// This function is not too performant as it uses some trigonometric operations.
4527    /// It is recommended to cache this value if you can.
4528    ///
4529    /// Alternatively, if you only want to know the speed, use
4530    /// [`get_speed_at_periapsis`][OrbitTrait::get_speed_at_periapsis] instead.  
4531    ///
4532    /// # Example
4533    /// ```
4534    /// use keplerian_sim::{Orbit, OrbitTrait};
4535    ///
4536    /// let mut orbit = Orbit::default();
4537    /// orbit.set_periapsis(100.0);
4538    /// orbit.set_eccentricity(0.5);
4539    ///
4540    /// let speed = orbit.get_speed_at_periapsis();
4541    /// let vel = orbit.get_velocity_at_periapsis();
4542    ///
4543    /// assert!((vel.length() - speed).abs() < 1e-15);
4544    /// ```
4545    ///
4546    /// # Speed vs. Velocity
4547    /// Speed is not to be confused with velocity.  
4548    /// Speed tells you how fast something is moving,
4549    /// while velocity tells you how fast *and in what direction* it's moving in.
4550    fn get_velocity_at_periapsis(&self) -> DVec3 {
4551        self.transform_pqw_vector(self.get_pqw_velocity_at_periapsis())
4552    }
4553
4554    /// Gets the velocity at the apoapsis of the orbit.
4555    ///
4556    /// # Performance
4557    /// This function is not too performant as it uses some trigonometric operations.
4558    /// It is recommended to cache this value if you can.
4559    ///
4560    /// Alternatively, if you only want to know the speed, use
4561    /// [`get_speed_at_apoapsis`][OrbitTrait::get_speed_at_apoapsis] instead.  
4562    ///
4563    /// # Open orbits (eccentricity >= 1)
4564    /// This function does not handle open orbits specially, and will return
4565    /// a non-physical value. You might want to use the getters for the velocity
4566    /// at the incoming and outgoing asymptotes:
4567    /// - [`get_velocity_at_incoming_asymptote`][OrbitTrait::get_velocity_at_incoming_asymptote]
4568    /// - [`get_velocity_at_outgoing_asymptote`][OrbitTrait::get_velocity_at_outgoing_asymptote]
4569    ///
4570    /// # Example
4571    /// ```
4572    /// use keplerian_sim::{Orbit, OrbitTrait};
4573    ///
4574    /// let mut orbit = Orbit::default();
4575    /// orbit.set_periapsis(100.0);
4576    /// orbit.set_eccentricity(0.5);
4577    ///
4578    /// let speed = orbit.get_speed_at_apoapsis();
4579    /// let vel = orbit.get_velocity_at_apoapsis();
4580    ///
4581    /// assert!((vel.length() - speed).abs() < 1e-15);
4582    /// ```
4583    ///
4584    /// # Speed vs. Velocity
4585    /// Speed is not to be confused with velocity.  
4586    /// Speed tells you how fast something is moving,
4587    /// while velocity tells you how fast *and in what direction* it's moving in.
4588    fn get_velocity_at_apoapsis(&self) -> DVec3 {
4589        self.transform_pqw_vector(self.get_pqw_velocity_at_apoapsis())
4590    }
4591
4592    /// Gets the velocity at the incoming asymptote of the trajectory.
4593    ///
4594    /// # Performance
4595    /// This function is not too performant as it uses some trigonometric operations.
4596    /// It is recommended to cache this value if you can.
4597    ///
4598    /// Alternatively, if you only want to know the speed, use
4599    /// [`get_speed_at_infinity`][OrbitTrait::get_speed_at_infinity] instead.  
4600    ///
4601    /// # Unchecked Operation
4602    /// This function does not check that the orbit is open.  
4603    /// This function will return a NaN vector for closed orbits (e < 1).
4604    ///
4605    /// # Example
4606    /// ```
4607    /// use keplerian_sim::{Orbit, OrbitTrait};
4608    ///
4609    /// let mut orbit = Orbit::default();
4610    /// orbit.set_periapsis(100.0);
4611    /// orbit.set_eccentricity(1.5);
4612    ///
4613    /// let speed = orbit.get_speed_at_infinity();
4614    /// let vel = orbit.get_velocity_at_incoming_asymptote();
4615    ///
4616    /// assert!((vel.length() - speed).abs() < 1e-15);
4617    /// ```
4618    ///
4619    /// # Speed vs. Velocity
4620    /// Speed is not to be confused with velocity.  
4621    /// Speed tells you how fast something is moving,
4622    /// while velocity tells you how fast *and in what direction* it's moving in.
4623    fn get_velocity_at_incoming_asymptote(&self) -> DVec3 {
4624        self.transform_pqw_vector(self.get_pqw_velocity_at_incoming_asymptote())
4625    }
4626
4627    /// Gets the velocity at the outgoing asymptote of the trajectory.
4628    ///
4629    /// # Performance
4630    /// This function is not too performant as it uses some trigonometric operations.
4631    /// It is recommended to cache this value if you can.
4632    ///
4633    /// Alternatively, if you only want to know the speed, use
4634    /// [`get_speed_at_infinity`][OrbitTrait::get_speed_at_infinity] instead.  
4635    ///
4636    /// # Unchecked Operation
4637    /// This function does not check that the orbit is open.  
4638    /// This function will return a NaN vector for closed orbits (e < 1).
4639    ///
4640    /// # Example
4641    /// ```
4642    /// use keplerian_sim::{Orbit, OrbitTrait};
4643    ///
4644    /// let mut orbit = Orbit::default();
4645    /// orbit.set_periapsis(100.0);
4646    /// orbit.set_eccentricity(1.5);
4647    ///
4648    /// let speed = orbit.get_speed_at_infinity();
4649    /// let vel = orbit.get_velocity_at_outgoing_asymptote();
4650    ///
4651    /// assert!((vel.length() - speed).abs() < 1e-15);
4652    /// ```
4653    ///
4654    /// # Speed vs. Velocity
4655    /// Speed is not to be confused with velocity.  
4656    /// Speed tells you how fast something is moving,
4657    /// while velocity tells you how fast *and in what direction* it's moving in.
4658    fn get_velocity_at_outgoing_asymptote(&self) -> DVec3 {
4659        self.transform_pqw_vector(self.get_pqw_velocity_at_outgoing_asymptote())
4660    }
4661
4662    /// Gets the velocity at a given eccentric anomaly in the orbit.
4663    ///
4664    /// # Example
4665    /// ```
4666    /// use keplerian_sim::{Orbit, OrbitTrait};
4667    ///
4668    /// let mut orbit = Orbit::default();
4669    /// orbit.set_periapsis(100.0);
4670    /// orbit.set_eccentricity(0.5);
4671    ///
4672    /// let vel_periapsis = orbit.get_velocity_at_true_anomaly(0.0);
4673    /// let vel_apoapsis = orbit.get_velocity_at_true_anomaly(std::f64::consts::PI);
4674    /// ```
4675    ///
4676    /// # Speed vs. Velocity
4677    /// Speed is not to be confused with velocity.  
4678    /// Speed tells you how fast something is moving,
4679    /// while velocity tells you how fast *and in what direction* it's moving in.
4680    ///
4681    /// # Performance
4682    /// This function is not too performant as it uses a few trigonometric
4683    /// operations.  
4684    /// It is recommended to cache this value if you can.
4685    ///
4686    /// Alternatively, if you just want to get the speed, consider using the
4687    /// [`get_speed_at_eccentric_anomaly`][OrbitTrait::get_speed_at_eccentric_anomaly]
4688    /// function instead.
4689    ///
4690    /// If you want to get both the position and velocity vectors, you can
4691    /// use the
4692    /// [`get_state_vectors_at_eccentric_anomaly`][OrbitTrait::get_state_vectors_at_eccentric_anomaly]
4693    /// function instead. It prevents redundant calculations and is therefore
4694    /// faster than calling the position and velocity functions separately.
4695    ///
4696    /// This function benefits significantly from being in the
4697    /// [cached version of the orbit struct][crate::Orbit].
4698    fn get_velocity_at_eccentric_anomaly(&self, eccentric_anomaly: f64) -> DVec3 {
4699        self.transform_pqw_vector(self.get_pqw_velocity_at_eccentric_anomaly(eccentric_anomaly))
4700    }
4701
4702    /// Gets the velocity at a given time in the orbit.
4703    ///
4704    /// # Time
4705    /// The time is expressed in seconds.
4706    ///
4707    /// # Performance
4708    /// The velocity is derived from the eccentric anomaly, which uses numerical
4709    /// methods and so is not performant.  
4710    /// It is recommended to cache this value if you can.
4711    ///
4712    /// Alternatively, if you only want to know the speed, use
4713    /// [`get_speed_at_time`][OrbitTrait::get_speed_at_time] instead.  
4714    /// Or, if you already have the eccentric anomaly or true anomaly, use the
4715    /// [`get_velocity_at_eccentric_anomaly`][OrbitTrait::get_velocity_at_eccentric_anomaly]
4716    /// and
4717    /// [`get_velocity_at_true_anomaly`][OrbitTrait::get_velocity_at_true_anomaly]
4718    /// functions instead.  
4719    /// These functions do not require numerical methods and therefore are a lot faster.
4720    ///
4721    /// If you want to get both the position and velocity vectors, you can
4722    /// use the
4723    /// [`get_state_vectors_at_time`][OrbitTrait::get_state_vectors_at_time]
4724    /// function instead. It prevents redundant calculations and is therefore
4725    /// faster than calling the position and velocity functions separately.
4726    ///
4727    /// # Speed vs. Velocity
4728    /// Speed is not to be confused with velocity.  
4729    /// Speed tells you how fast something is moving,
4730    /// while velocity tells you how fast *and in what direction* it's moving in.
4731    fn get_velocity_at_time(&self, time: f64) -> DVec3 {
4732        self.get_velocity_at_eccentric_anomaly(self.get_eccentric_anomaly_at_time(time))
4733    }
4734
4735    /// Gets the altitude of the body from its parent at a given angle (true anomaly) in the orbit.
4736    ///
4737    /// # Angle
4738    /// The angle is expressed in radians, and ranges from 0 to tau.  
4739    /// Anything out of range will get wrapped around.
4740    ///
4741    /// Note that some angles, even within 0 to tau, are impossible for
4742    /// hyperbolic orbits and may result in invalid values.
4743    /// Check for the range of angles for a hyperbolic orbit using
4744    /// [`get_true_anomaly_at_asymptote`][OrbitTrait::get_true_anomaly_at_asymptote].
4745    ///
4746    /// # Altitude
4747    /// The altitude is measured in meters, and measured from the
4748    /// center of the parent body (origin).
4749    ///
4750    /// # Performance
4751    /// This function is performant, however, if you already
4752    /// know the orbit's semi-latus rectum or the cosine of the true anomaly,
4753    /// you can use the
4754    /// [`get_altitude_at_true_anomaly_unchecked`][OrbitTrait::get_altitude_at_true_anomaly_unchecked]
4755    /// function to skip a few steps in the calculation.
4756    ///
4757    /// # Example
4758    /// ```
4759    /// use keplerian_sim::{Orbit, OrbitTrait};
4760    ///
4761    /// let mut orbit = Orbit::default();
4762    /// orbit.set_periapsis(100.0);
4763    /// orbit.set_eccentricity(0.0);
4764    ///
4765    /// let altitude = orbit.get_altitude_at_true_anomaly(0.0);
4766    ///
4767    /// assert_eq!(altitude, 100.0);
4768    /// ```
4769    #[doc(alias = "get_altitude_at_angle")]
4770    fn get_altitude_at_true_anomaly(&self, true_anomaly: f64) -> f64 {
4771        self.get_altitude_at_true_anomaly_unchecked(
4772            self.get_semi_latus_rectum(),
4773            true_anomaly.cos(),
4774        )
4775    }
4776
4777    /// Gets the altitude of the body from its parent given the
4778    /// cosine of the true anomaly.  
4779    ///
4780    /// This function should only be used if you already know the semi-latus
4781    /// rectum or `cos(true_anomaly)` beforehand, and want to minimize
4782    /// duplicated work.
4783    ///
4784    /// # Unchecked Operation
4785    /// This function does not perform any checks on the validity
4786    /// of the `cos_true_anomaly` parameter. Invalid values result in
4787    /// possibly-nonsensical output values.
4788    ///
4789    /// # Altitude
4790    /// The altitude is measured in meters, and measured from the
4791    /// center of the parent body (origin).
4792    ///
4793    /// # Angle
4794    /// The angle is expressed in radians, and ranges from 0 to tau.  
4795    /// Anything out of range will get wrapped around.
4796    ///
4797    /// Note that some angles, even within 0 to tau, are impossible for
4798    /// hyperbolic orbits and may result in invalid values.
4799    /// Check for the range of angles for a hyperbolic orbit using
4800    /// [`get_true_anomaly_at_asymptote`][OrbitTrait::get_true_anomaly_at_asymptote].
4801    ///
4802    /// # Performance
4803    /// This function, by itself, is performant and is unlikely
4804    /// to be the culprit of any performance issues.
4805    ///
4806    /// # Example
4807    /// ```
4808    /// use keplerian_sim::{Orbit, OrbitTrait};
4809    ///
4810    /// let mut orbit = Orbit::default();
4811    /// orbit.set_periapsis(100.0);
4812    /// orbit.set_eccentricity(0.5);
4813    ///
4814    /// let true_anomaly = 1.2345f64;
4815    ///
4816    /// // Precalculate some values...
4817    /// # let semi_latus_rectum = orbit.get_semi_latus_rectum();
4818    /// # let cos_true_anomaly = true_anomaly.cos();
4819    ///
4820    /// // Scenario 1: If you know just the semi-latus rectum
4821    /// let scenario_1 = orbit.get_altitude_at_true_anomaly_unchecked(
4822    ///     semi_latus_rectum, // We pass in our precalculated SLR...
4823    ///     true_anomaly.cos() // but calculate the cosine
4824    /// );
4825    ///
4826    /// // Scenario 2: If you know just the cosine of the true anomaly
4827    /// let scenario_2 = orbit.get_altitude_at_true_anomaly_unchecked(
4828    ///     orbit.get_semi_latus_rectum(), // We calculate the SLR...
4829    ///     cos_true_anomaly // but use our precalculated cosine
4830    /// );
4831    ///
4832    /// // Scenario 3: If you know both the semi-latus rectum:
4833    /// let scenario_3 = orbit.get_altitude_at_true_anomaly_unchecked(
4834    ///     semi_latus_rectum, // We pass in our precalculated SLR...
4835    ///     cos_true_anomaly // AND use our precalculated cosine
4836    /// );
4837    ///
4838    /// assert_eq!(scenario_1, scenario_2);
4839    /// assert_eq!(scenario_2, scenario_3);
4840    /// assert_eq!(scenario_3, orbit.get_altitude_at_true_anomaly(true_anomaly));
4841    /// ```
4842    fn get_altitude_at_true_anomaly_unchecked(
4843        &self,
4844        semi_latus_rectum: f64,
4845        cos_true_anomaly: f64,
4846    ) -> f64 {
4847        semi_latus_rectum / (1.0 + self.get_eccentricity() * cos_true_anomaly)
4848    }
4849
4850    /// Gets the altitude at a given eccentric anomaly in the orbit.
4851    ///
4852    /// # Altitude
4853    /// The altitude is measured in meters, and measured from the
4854    /// center of the parent body (origin).
4855    ///
4856    /// # Performance
4857    /// This function is not too performant as it uses a few trigonometric operations.
4858    /// It is recommended to cache this value if you can.
4859    ///
4860    /// Alternatively, if you already know the true anomaly, use the
4861    /// [`get_altitude_at_true_anomaly`][OrbitTrait::get_altitude_at_true_anomaly]
4862    /// function instead.
4863    ///
4864    /// # Example
4865    /// ```
4866    /// use keplerian_sim::{Orbit, OrbitTrait};
4867    ///
4868    /// let mut orbit = Orbit::default();
4869    /// orbit.set_periapsis(100.0);
4870    /// orbit.set_eccentricity(0.0);
4871    ///
4872    /// let altitude = orbit.get_altitude_at_eccentric_anomaly(0.0);
4873    ///
4874    /// assert_eq!(altitude, 100.0);
4875    /// ```
4876    fn get_altitude_at_eccentric_anomaly(&self, eccentric_anomaly: f64) -> f64 {
4877        self.get_altitude_at_true_anomaly(
4878            self.get_true_anomaly_at_eccentric_anomaly(eccentric_anomaly),
4879        )
4880    }
4881
4882    // TODO: DOC: POST-PARABOLIC SUPPORT: Update doc
4883    /// Gets the altitude of the body from its parent at a given time in the orbit.
4884    ///
4885    /// Note that due to floating-point imprecision, values of extreme
4886    /// magnitude may not be accurate.
4887    ///
4888    /// # Time
4889    /// The time is expressed in seconds.
4890    ///
4891    /// # Altitude
4892    /// The altitude is measured in meters, and measured from the
4893    /// center of the parent body (origin).
4894    ///
4895    /// # Performance
4896    /// This involves calculating the true anomaly at a given time, and so is not very performant.  
4897    /// It is recommended to cache this value when possible.
4898    ///
4899    /// Alternatively, if you already know the eccentric anomaly or the true anomaly,
4900    /// consider using the
4901    /// [`get_altitude_at_eccentric_anomaly`][OrbitTrait::get_altitude_at_eccentric_anomaly]
4902    /// and
4903    /// [`get_altitude_at_true_anomaly`][OrbitTrait::get_altitude_at_true_anomaly]
4904    /// functions instead.  
4905    /// Those do not use numerical methods and therefore are a lot faster.
4906    ///
4907    /// # Parabolic Support
4908    /// **This function returns infinity for parabolic orbits** due to how the equation for
4909    /// true anomaly works.
4910    fn get_altitude_at_time(&self, time: f64) -> f64 {
4911        self.get_altitude_at_true_anomaly(self.get_true_anomaly_at_time(time))
4912    }
4913
4914    // TODO: DOC: POST-PARABOLIC SUPPORT: Update doc
4915    /// Gets the 3D position at a given time in the orbit.
4916    ///
4917    /// # Time
4918    /// The time is expressed in seconds.
4919    ///
4920    /// # Performance
4921    /// This involves calculating the true anomaly at a given time,
4922    /// and so is not very performant.  
4923    /// It is recommended to cache this value when possible.
4924    ///
4925    /// This function benefits significantly from being in the
4926    /// [cached version of the orbit struct][crate::Orbit].  
4927    ///
4928    /// Alternatively, if you already know the true anomaly,
4929    /// consider using the
4930    /// [`get_position_at_true_anomaly`][OrbitTrait::get_position_at_true_anomaly]
4931    /// function instead.  
4932    /// That does not use numerical methods and therefore is a lot faster.
4933    ///
4934    /// If you want to get both the position and velocity vectors, you can
4935    /// use the
4936    /// [`get_state_vectors_at_time`][OrbitTrait::get_state_vectors_at_time]
4937    /// function instead. It prevents redundant calculations and is therefore
4938    /// faster than calling the position and velocity functions separately.
4939    ///
4940    /// # Parabolic Support
4941    /// **This function returns non-finite numbers for parabolic orbits**
4942    /// due to how the equation for true anomaly works.
4943    fn get_position_at_time(&self, time: f64) -> DVec3 {
4944        self.get_position_at_true_anomaly(self.get_true_anomaly_at_time(time))
4945    }
4946
4947    // TODO: DOC: POST-PARABOLIC SUPPORT: Update doc
4948    /// Gets the 3D position and velocity at a given eccentric anomaly in the orbit.
4949    ///
4950    /// # Performance
4951    /// This function uses several trigonometric functions, and so it is not too performant.  
4952    /// It is recommended to cache this value if you can.
4953    ///
4954    /// This is, however, faster than individually calling the position and velocity getters
4955    /// separately, as this will reuse calculations whenever possible.
4956    ///
4957    /// If you need *only one* of the vectors, though, you should instead call the dedicated
4958    /// getters:  
4959    /// [`get_velocity_at_eccentric_anomaly`][OrbitTrait::get_velocity_at_eccentric_anomaly]  
4960    /// [`get_position_at_eccentric_anomaly`][OrbitTrait::get_position_at_eccentric_anomaly]  
4961    ///
4962    /// This function should give similar performance to the getter from the true anomaly:  
4963    /// [`get_state_vectors_at_true_anomaly`][OrbitTrait::get_state_vectors_at_true_anomaly]
4964    ///
4965    /// In case you really want to, an unchecked version of this function is available:  
4966    /// [`get_state_vectors_from_unchecked_parts`][OrbitTrait::get_state_vectors_from_unchecked_parts]
4967    ///
4968    /// # Parabolic Support
4969    /// This function doesn't support parabolic trajectories yet.  
4970    /// `NaN`s or nonsensical values may be returned.
4971    fn get_state_vectors_at_eccentric_anomaly(&self, eccentric_anomaly: f64) -> StateVectors {
4972        let semi_major_axis = self.get_semi_major_axis();
4973        let sqrt_abs_gm_a = (semi_major_axis * self.get_gravitational_parameter())
4974            .abs()
4975            .sqrt();
4976        let true_anomaly = self.get_true_anomaly_at_eccentric_anomaly(eccentric_anomaly);
4977        let sincos_angle = true_anomaly.sin_cos();
4978        let eccentricity = self.get_eccentricity();
4979
4980        let semi_latus_rectum = self.get_periapsis() * (1.0 + self.get_eccentricity());
4981
4982        let altitude =
4983            self.get_altitude_at_true_anomaly_unchecked(semi_latus_rectum, sincos_angle.1);
4984
4985        let q_mult = (1.0 - eccentricity.powi(2)).abs().sqrt();
4986
4987        // TODO: PARABOLIC SUPPORT: This function doesn't yet consider parabolic orbits.
4988        let trig_ecc_anom = if eccentricity < 1.0 {
4989            eccentric_anomaly.sin_cos()
4990        } else {
4991            sinhcosh(eccentric_anomaly)
4992        };
4993
4994        self.get_state_vectors_from_unchecked_parts(
4995            sqrt_abs_gm_a,
4996            altitude,
4997            q_mult,
4998            trig_ecc_anom,
4999            sincos_angle,
5000        )
5001    }
5002
5003    /// Gets the 3D position and velocity at a given angle (true anomaly) in the orbit.
5004    ///
5005    /// # Angle
5006    /// The angle is expressed in radians, and ranges from 0 to tau.  
5007    /// Anything out of range will get wrapped around.
5008    ///
5009    /// # Performance
5010    /// This function uses several trigonometric functions, and so it is not too performant.  
5011    /// It is recommended to cache this value if you can.
5012    ///
5013    /// This is, however, faster than individually calling the position and velocity getters
5014    /// separately, as this will reuse calculations whenever possible.
5015    ///
5016    /// If you need *only one* of the vectors, though, you should instead call the dedicated
5017    /// getters:  
5018    /// [`get_velocity_at_true_anomaly`][OrbitTrait::get_velocity_at_true_anomaly]
5019    /// [`get_position_at_true_anomaly`][OrbitTrait::get_position_at_true_anomaly]
5020    ///
5021    /// This function should give similar performance to the getter from the eccentric anomaly:  
5022    /// [`get_state_vectors_at_eccentric_anomaly`][OrbitTrait::get_state_vectors_at_true_anomaly]
5023    ///
5024    /// In case you really want to, an unchecked version of this function is available:  
5025    /// [`get_state_vectors_from_unchecked_parts`][OrbitTrait::get_state_vectors_from_unchecked_parts]
5026    ///
5027    /// # Parabolic Support
5028    /// This function doesn't support parabolic trajectories yet.  
5029    /// `NaN`s or nonsensical values may be returned.
5030    fn get_state_vectors_at_true_anomaly(&self, true_anomaly: f64) -> StateVectors {
5031        let semi_major_axis = self.get_semi_major_axis();
5032        let sqrt_abs_gm_a = (semi_major_axis * self.get_gravitational_parameter())
5033            .abs()
5034            .sqrt();
5035        let eccentric_anomaly = self.get_eccentric_anomaly_at_true_anomaly(true_anomaly);
5036        let sincos_angle = true_anomaly.sin_cos();
5037        let eccentricity = self.get_eccentricity();
5038
5039        let semi_latus_rectum = self.get_periapsis() * (1.0 + self.get_eccentricity());
5040
5041        let altitude =
5042            self.get_altitude_at_true_anomaly_unchecked(semi_latus_rectum, sincos_angle.1);
5043
5044        let q_mult = (1.0 - eccentricity.powi(2)).abs().sqrt();
5045
5046        // TODO: PARABOLIC SUPPORT: This function doesn't yet consider parabolic orbits.
5047        let trig_ecc_anom = if eccentricity < 1.0 {
5048            eccentric_anomaly.sin_cos()
5049        } else {
5050            sinhcosh(eccentric_anomaly)
5051        };
5052
5053        self.get_state_vectors_from_unchecked_parts(
5054            sqrt_abs_gm_a,
5055            altitude,
5056            q_mult,
5057            trig_ecc_anom,
5058            sincos_angle,
5059        )
5060    }
5061
5062    /// Gets the 3D position and velocity at a given mean anomaly in the orbit.
5063    ///
5064    /// # Performance
5065    /// This function involves converting the mean anomaly to an eccentric anomaly,
5066    /// which involves numerical approach methods and are therefore not performant.  
5067    /// It is recommended to cache this value if you can.  
5068    ///
5069    /// Alternatively, if you already know the eccentric anomaly or true anomaly,
5070    /// use the following functions instead, which do not use numerical methods and
5071    /// therefore are significantly faster:  
5072    /// [`get_state_vectors_at_eccentric_anomaly`][OrbitTrait::get_state_vectors_at_eccentric_anomaly]
5073    /// [`get_state_vectors_at_true_anomaly`][OrbitTrait::get_state_vectors_at_true_anomaly]
5074    ///
5075    /// This function is faster than individually calling the position and velocity getters
5076    /// separately, as this will reuse calculations whenever possible.
5077    ///
5078    /// # Parabolic Support
5079    /// This function doesn't support parabolic trajectories yet.  
5080    /// `NaN`s or nonsensical values may be returned.
5081    fn get_state_vectors_at_mean_anomaly(&self, mean_anomaly: f64) -> StateVectors {
5082        self.get_state_vectors_at_eccentric_anomaly(
5083            self.get_eccentric_anomaly_at_mean_anomaly(mean_anomaly),
5084        )
5085    }
5086
5087    /// Gets the 3D position and velocity at a given time in the orbit.
5088    ///
5089    /// # Time
5090    /// The time is measured in seconds.
5091    ///
5092    /// # Performance
5093    /// This function involves converting a mean anomaly (derived from the time)
5094    /// into an eccentric anomaly.  
5095    /// This involves numerical approach methods and are therefore not performant.  
5096    /// It is recommended to cache this value if you can.
5097    ///
5098    /// Alternatively, if you already know the eccentric anomaly or true anomaly,
5099    /// use the following functions instead, which do not use numerical methods and
5100    /// therefore are significantly faster:  
5101    /// [`get_state_vectors_at_eccentric_anomaly`][OrbitTrait::get_state_vectors_at_eccentric_anomaly]
5102    /// [`get_state_vectors_at_true_anomaly`][OrbitTrait::get_state_vectors_at_true_anomaly]
5103    ///
5104    /// If you only know the mean anomaly, then that may help with performance a little bit,
5105    /// in which case you can use
5106    /// [`get_state_vectors_at_mean_anomaly`][OrbitTrait::get_state_vectors_at_mean_anomaly]
5107    /// instead.
5108    ///
5109    /// This function is faster than individually calling the position and velocity getters
5110    /// separately, as this will reuse calculations whenever possible.
5111    ///
5112    /// If you need *only one* of the vectors, though, you should instead call the dedicated
5113    /// getters:  
5114    /// [`get_velocity_at_time`][OrbitTrait::get_velocity_at_time]
5115    /// [`get_position_at_time`][OrbitTrait::get_position_at_time]
5116    ///
5117    /// # Parabolic Support
5118    /// This function doesn't support parabolic trajectories yet.  
5119    /// `NaN`s or nonsensical values may be returned.
5120    fn get_state_vectors_at_time(&self, time: f64) -> StateVectors {
5121        self.get_state_vectors_at_mean_anomaly(self.get_mean_anomaly_at_time(time))
5122    }
5123
5124    /// Gets the 3D position and velocity at a certain point in the orbit.
5125    ///
5126    /// # Unchecked Operation
5127    /// This function does not check the validity of the inputs.  
5128    /// Invalid inputs may lead to nonsensical results.
5129    ///
5130    /// # Parameters
5131    /// ## `sqrt_abs_gm_a`
5132    /// This parameter's value should be calculated using the formula:  
5133    /// `sqrt(abs(GM * a))`  
5134    /// where:
5135    /// - `GM` is the gravitational parameter (a.k.a. mu)
5136    /// - `a` is the semi-major axis of the orbit
5137    ///
5138    /// Alternatively, for elliptic orbits, this formula can be used:  
5139    /// `sqrt(GM * a)`
5140    ///
5141    /// As for hyperbolic orbits, this formula can be used:  
5142    /// `sqrt(-GM * a)`
5143    ///
5144    /// ## `altitude`
5145    /// The altitude at that point in the orbit.  
5146    /// The altitude is measured in meters, and measured from the
5147    /// center of the parent body (origin).
5148    ///
5149    /// ## `q_mult`
5150    /// This parameter is a multiplier for the second element in the velocity PQW vector.  
5151    /// For elliptic orbits, it should be calculated by the formula `sqrt(1 - e^2)`,
5152    /// where `e` is the eccentricity of the orbit.  
5153    /// For hyperbolic orbits, it should be calculated by the formula `sqrt(e^2 - 1)`,
5154    /// where `e` is the eccentricity of the orbit.  
5155    /// Alternatively, for the general case, you can use the formula `sqrt(abs(1 - e^2))`.
5156    ///
5157    /// ## `trig_ecc_anom`
5158    /// **For elliptic orbits**, this parameter should be a tuple containing the sine and cosine
5159    /// values of the eccentric anomaly, respectively.  
5160    /// **For hyperbolic orbits**, this parameter should be a tuple containing the **hyperbolic**
5161    /// sine and **hyperbolic** cosine values of the eccentric anomaly, respectively.
5162    ///
5163    /// ## `sincos_angle`
5164    /// This parameter should be calculated by passing the true anomaly into sin_cos():
5165    /// ```
5166    /// let true_anomaly: f64 = 1.25; // Example value
5167    /// let sincos_angle = true_anomaly.sin_cos();
5168    /// ```
5169    ///
5170    /// # Performance
5171    /// This function, by itself, is very performant and should not
5172    /// be the cause of any performance issues.
5173    ///
5174    /// # Parabolic Support
5175    /// This function doesn't support parabolic trajectories yet.  
5176    /// `NaN`s or nonsensical values may be returned.
5177    ///
5178    /// # Example
5179    /// ```
5180    /// use keplerian_sim::{Orbit, OrbitTrait, StateVectors};
5181    ///
5182    /// # fn main() {
5183    /// // Elliptic (circular) case
5184    ///
5185    /// let orbit = Orbit::default();
5186    /// let true_anomaly: f64 = 1.24; // Example value
5187    /// let eccentric_anomaly = orbit
5188    ///     .get_eccentric_anomaly_at_true_anomaly(true_anomaly);
5189    /// let sqrt_abs_gm_a = (
5190    ///     orbit.get_gravitational_parameter() *
5191    ///     orbit.get_semi_major_axis()
5192    /// ).abs().sqrt();
5193    /// let altitude = orbit.get_altitude_at_true_anomaly(true_anomaly);
5194    /// let q_mult = (
5195    ///     1.0 - orbit.get_eccentricity().powi(2)
5196    /// ).abs().sqrt();
5197    /// let trig_ecc_anom = eccentric_anomaly.sin_cos();
5198    /// let sincos_angle = true_anomaly.sin_cos();
5199    ///
5200    /// let sv = StateVectors {
5201    ///     position: orbit.get_position_at_true_anomaly(true_anomaly),
5202    ///     velocity: orbit.get_velocity_at_true_anomaly(true_anomaly),
5203    /// };
5204    ///
5205    /// let sv2 = orbit.get_state_vectors_from_unchecked_parts(
5206    ///     sqrt_abs_gm_a,
5207    ///     altitude,
5208    ///     q_mult,
5209    ///     trig_ecc_anom,
5210    ///     sincos_angle
5211    /// );
5212    ///
5213    /// assert_eq!(sv, sv2);
5214    /// # }
5215    /// ```
5216    /// ```
5217    /// use keplerian_sim::{Orbit, OrbitTrait, sinhcosh, StateVectors};
5218    ///
5219    /// # fn main() {
5220    /// // Hyperbolic case
5221    ///
5222    /// let mut orbit = Orbit::default();
5223    /// orbit.set_eccentricity(1.5);
5224    /// let true_anomaly: f64 = 1.24; // Example value
5225    /// let eccentric_anomaly = orbit
5226    ///     .get_eccentric_anomaly_at_true_anomaly(true_anomaly);
5227    /// let sqrt_abs_gm_a = (
5228    ///     orbit.get_gravitational_parameter() *
5229    ///     orbit.get_semi_major_axis()
5230    /// ).abs().sqrt();
5231    /// let altitude = orbit.get_altitude_at_true_anomaly(true_anomaly);
5232    /// let q_mult = (
5233    ///     1.0 - orbit.get_eccentricity().powi(2)
5234    /// ).abs().sqrt();
5235    /// let trig_ecc_anom = sinhcosh(eccentric_anomaly);
5236    /// let sincos_angle = true_anomaly.sin_cos();
5237    ///
5238    /// let sv = StateVectors {
5239    ///     position: orbit.get_position_at_true_anomaly(true_anomaly),
5240    ///     velocity: orbit.get_velocity_at_true_anomaly(true_anomaly),
5241    /// };
5242    ///
5243    /// let sv2 = orbit.get_state_vectors_from_unchecked_parts(
5244    ///     sqrt_abs_gm_a,
5245    ///     altitude,
5246    ///     q_mult,
5247    ///     trig_ecc_anom,
5248    ///     sincos_angle
5249    /// );
5250    ///
5251    /// assert_eq!(sv, sv2);
5252    /// # }
5253    /// ```
5254    fn get_state_vectors_from_unchecked_parts(
5255        &self,
5256        sqrt_abs_gm_a: f64,
5257        altitude: f64,
5258        q_mult: f64,
5259        trig_ecc_anom: (f64, f64),
5260        sincos_angle: (f64, f64),
5261    ) -> StateVectors {
5262        let outer_mult = sqrt_abs_gm_a / altitude;
5263        let pqw_velocity =
5264            self.get_pqw_velocity_at_eccentric_anomaly_unchecked(outer_mult, q_mult, trig_ecc_anom);
5265        let pqw_position = self.get_pqw_position_at_true_anomaly_unchecked(altitude, sincos_angle);
5266        let matrix = self.get_transformation_matrix();
5267        StateVectors {
5268            position: matrix.dot_vec(pqw_position),
5269            velocity: matrix.dot_vec(pqw_velocity),
5270        }
5271    }
5272
5273    /// Transforms a position from the perifocal coordinate (PQW) system into
5274    /// 3D, using the orbital parameters.
5275    ///
5276    /// # Perifocal Coordinate (PQW) System
5277    /// The perifocal coordinate (PQW) system is a frame of reference using
5278    /// the basis vectors p-hat, q-hat, and w-hat, where p-hat points to the
5279    /// periapsis, q-hat has a true anomaly 90 degrees more than p-hat, and
5280    /// w-hat points perpendicular to the orbital plane.
5281    ///
5282    /// Learn more: <https://en.wikipedia.org/wiki/Perifocal_coordinate_system>
5283    ///
5284    /// # Performance
5285    /// This function performs 10x faster in the cached version of the
5286    /// [`Orbit`] struct, as it doesn't need to recalculate the transformation
5287    /// matrix needed to transform 2D vector.
5288    fn transform_pqw_vector(&self, position: DVec2) -> DVec3 {
5289        self.get_transformation_matrix().dot_vec(position)
5290    }
5291
5292    /// Gets the true anomaly where a certain altitude is reached.
5293    ///
5294    /// Returns NaN if the orbit is circular or there are no solutions.
5295    ///
5296    /// # Altitude
5297    /// The altitude is measured in meters, and measured from the
5298    /// center of the parent body (origin).
5299    ///
5300    /// The altitude given to this function should be between
5301    /// the periapsis (minimum) and the apoapsis (maximum).  
5302    /// Anything out of range will return NaN.
5303    ///
5304    /// In the case of hyperbolic orbits, there is no maximum,
5305    /// but the altitude should be positive and more than the periapsis.  
5306    /// Although there technically is a mathematical solution for "negative altitudes"
5307    /// between negative infinity and the apoapsis (which in this case is negative),
5308    /// they may not be very useful in most scenarios.
5309    ///
5310    /// # Domain
5311    /// This function returns a float between 0 and π, unless if
5312    /// it returns NaN.  
5313    /// Do note that, although this is the principal solution,
5314    /// other solutions exist, and may be desired. There exists an
5315    /// alternate solution when you negate the principal solution,
5316    /// and the solutions repeat every 2π.
5317    ///
5318    /// ## Example
5319    /// If there is a principal solution at `1`, that means there
5320    /// is an alternate solution at `-1`, and there are also
5321    /// solutions `2π + 1`, `2π - 1`, `4π + 1`, `4π - 1`, etc.
5322    ///
5323    /// # Performance
5324    /// This function is moderately performant and is unlikely to be
5325    /// the culprit of any performance issues.
5326    ///
5327    /// However, if you already computed the semi-latus rectum or the
5328    /// reciprocal of the eccentricity, you may use the unchecked version
5329    /// of this function for a small performance boost:  
5330    /// [`get_true_anomaly_at_altitude_unchecked`][OrbitTrait::get_true_anomaly_at_altitude_unchecked]
5331    #[doc(alias = "get_angle_at_altitude")]
5332    fn get_true_anomaly_at_altitude(&self, altitude: f64) -> f64 {
5333        self.get_true_anomaly_at_altitude_unchecked(
5334            self.get_semi_latus_rectum(),
5335            altitude,
5336            self.get_eccentricity().recip(),
5337        )
5338    }
5339
5340    /// Gets the true anomaly where a certain altitude is reached.
5341    ///
5342    /// Returns NaN if the orbit is circular or there are no solutions.
5343    ///
5344    /// # Altitude
5345    /// The altitude is measured in meters, and measured from the
5346    /// center of the parent body (origin).
5347    ///
5348    /// The altitude given to this function should be between
5349    /// the periapsis (minimum) and the apoapsis (maximum).  
5350    /// Anything out of range will return NaN.
5351    ///
5352    /// In the case of hyperbolic orbits, there is no maximum,
5353    /// but the altitude should be positive and more than the periapsis.  
5354    /// Although there technically is a mathematical solution for "negative altitudes"
5355    /// between negative infinity and the apoapsis (which in this case is negative),
5356    /// they may not be very useful in most scenarios.
5357    ///
5358    /// # Unchecked Operation
5359    /// This function does not check the validity of the inputted
5360    /// values. Nonsensical/invalid inputs may result in
5361    /// nonsensical/invalid outputs.
5362    ///
5363    /// # Domain
5364    /// This function returns a float between 0 and π, unless if
5365    /// it returns NaN.  
5366    /// Do note that, although this is the principal solution,
5367    /// other solutions exist, and may be desired. There exists an
5368    /// alternate solution when you negate the principal solution,
5369    /// and the solutions repeat every 2π.
5370    ///
5371    /// ## Example
5372    /// If there is a principal solution at `1`, that means there
5373    /// is an alternate solution at `-1`, and there are also
5374    /// solutions `2π + 1`, `2π - 1`, `4π + 1`, `4π - 1`, etc.
5375    ///
5376    /// # Performance
5377    /// This function is moderately performant and is unlikely to be
5378    /// the culprit of any performance issues.
5379    #[doc(alias = "get_angle_at_altitude_unchecked")]
5380    fn get_true_anomaly_at_altitude_unchecked(
5381        &self,
5382        semi_latus_rectum: f64,
5383        altitude: f64,
5384        eccentricity_recip: f64,
5385    ) -> f64 {
5386        // r = p / (1 + e cos ν), r > 0, p > 0.
5387        // 1 / r = (1 + e cos ν) / p
5388        // 1 / r = 1 / p + (e cos ν / p)
5389        // 1 / r - 1 / p = e cos ν / p
5390        // e cos ν / p = 1 / r - 1 / p
5391        // e cos ν = p (1 / r - 1 / p)
5392        // e cos ν = p / r - 1
5393        // cos ν = (p / r - 1) / e
5394        // ν = acos((p / r - 1) / e)
5395        ((semi_latus_rectum / altitude - 1.0) * eccentricity_recip).acos()
5396    }
5397
5398    /// Gets the eccentricity of the orbit.
5399    ///
5400    /// The eccentricity of an orbit is a measure of how much it deviates
5401    /// from a perfect circle.
5402    ///
5403    /// An eccentricity of 0 means the orbit is a perfect circle.  
5404    /// Between 0 and 1, the orbit is elliptic, and has an oval shape.  
5405    /// An orbit with an eccentricity of 1 is said to be parabolic.  
5406    /// If it's greater than 1, the orbit is hyperbolic.
5407    ///
5408    /// For hyperbolic trajectories, the higher the eccentricity, the
5409    /// straighter the path.
5410    ///
5411    /// Wikipedia on conic section eccentricity: <https://en.wikipedia.org/wiki/Eccentricity_(mathematics)>  
5412    /// (Keplerian orbits are conic sections, so the concepts still apply)
5413    fn get_eccentricity(&self) -> f64;
5414
5415    /// Gets whether or not the orbit is circular.
5416    ///
5417    /// A circular orbit has an eccentricity of 0.
5418    #[inline]
5419    fn is_circular(&self) -> bool {
5420        self.get_eccentricity() == 0.0
5421    }
5422
5423    /// Gets whether or not the orbit is strictly elliptic.
5424    ///
5425    /// A strictly elliptic orbit has an eccentricity between
5426    /// 0 and 1.
5427    ///
5428    /// For getting whether or not the orbit is circular
5429    /// or elliptic, use [`is_closed`][OrbitTrait::is_closed]
5430    #[inline]
5431    fn is_elliptic(&self) -> bool {
5432        let eccentricity = self.get_eccentricity();
5433
5434        eccentricity < 1.0 && eccentricity > 0.0
5435    }
5436
5437    /// Gets whether or not the orbit is closed.
5438    ///
5439    /// A closed orbit can be either circular or elliptic,
5440    /// i.e. has an eccentricity of less than 1.
5441    ///
5442    /// For getting whether or not the orbit is circular (e = 0),
5443    /// use [`is_circular`][OrbitTrait::is_circular].
5444    ///
5445    /// For getting whether or not the orbit is strictly
5446    /// elliptic (0 < e < 1), use [`is_elliptic`][OrbitTrait::is_elliptic].
5447    #[inline]
5448    fn is_closed(&self) -> bool {
5449        self.get_eccentricity() < 1.0
5450    }
5451
5452    /// Gets whether or not the trajectory is parabolic.
5453    ///
5454    /// A parabolic trajectory has an eccentricity of exactly 1.
5455    #[inline]
5456    fn is_parabolic(&self) -> bool {
5457        self.get_eccentricity() == 1.0
5458    }
5459
5460    /// Gets whether or not the trajectory is hyperbolic.
5461    ///
5462    /// A hyperbolic trajectory has an eccentricity of greater than 1.
5463    ///
5464    /// For getting whether or not the trajectory is open — i.e.,
5465    /// has an eccentricity of *at least* 1 — use
5466    /// [`is_open`][OrbitTrait::is_open].
5467    #[inline]
5468    fn is_hyperbolic(&self) -> bool {
5469        self.get_eccentricity() > 1.0
5470    }
5471
5472    /// Gets whether or not the trajectory is open.
5473    ///
5474    /// An open trajectory has an eccentricity of at least 1, i.e.,
5475    /// is either a parabolic trajectory or a hyperbolic trajectory.
5476    ///
5477    /// For getting whether or not a trajectory is parabolic (e = 1),
5478    /// use [`is_parabolic`][OrbitTrait::is_parabolic].
5479    ///
5480    /// For getting whether or not a trajectory is hyperbolic (e > 1),
5481    /// use [`is_hyperbolic`][OrbitTrait::is_hyperbolic].
5482    #[inline]
5483    fn is_open(&self) -> bool {
5484        self.get_eccentricity() >= 1.0
5485    }
5486
5487    /// Sets the eccentricity of the orbit.
5488    ///
5489    /// The eccentricity of an orbit is a measure of how much it deviates
5490    /// from a perfect circle.
5491    ///
5492    /// An eccentricity of 0 means the orbit is a perfect circle.  
5493    /// Between 0 and 1, the orbit is elliptic, and has an oval shape.  
5494    /// An orbit with an eccentricity of 1 is said to be parabolic.  
5495    /// If it's greater than 1, the orbit is hyperbolic.
5496    ///
5497    /// For hyperbolic trajectories, the higher the eccentricity, the
5498    /// straighter the path.
5499    ///
5500    /// Wikipedia on conic section eccentricity: <https://en.wikipedia.org/wiki/Eccentricity_(mathematics)>  
5501    /// (Keplerian orbits are conic sections, so the concepts still apply)
5502    fn set_eccentricity(&mut self, eccentricity: f64);
5503
5504    /// Gets the periapsis of the orbit.
5505    ///
5506    /// The periapsis of an orbit is the distance at the closest point
5507    /// to the parent body.
5508    ///
5509    /// More simply, this is the "minimum altitude" of an orbit.
5510    ///
5511    /// Wikipedia: <https://en.wikipedia.org/wiki/Apsis>
5512    fn get_periapsis(&self) -> f64;
5513
5514    /// Sets the periapsis of the orbit.
5515    ///
5516    /// The periapsis of an orbit is the distance at the closest point
5517    /// to the parent body.
5518    ///
5519    /// More simply, this is the "minimum altitude" of an orbit.
5520    ///
5521    /// Wikipedia: <https://en.wikipedia.org/wiki/Apsis>
5522    fn set_periapsis(&mut self, periapsis: f64);
5523
5524    /// Gets the inclination of the orbit in radians.
5525    ///
5526    /// The inclination of an orbit is the angle between the plane of the
5527    /// orbit and the reference plane.
5528    ///
5529    /// In simple terms, it tells you how "tilted" the orbit is.
5530    ///
5531    /// Wikipedia: <https://en.wikipedia.org/wiki/Orbital_inclination>
5532    fn get_inclination(&self) -> f64;
5533
5534    /// Sets the inclination of the orbit in radians.
5535    ///
5536    /// The inclination of an orbit is the angle between the plane of the
5537    /// orbit and the reference plane.
5538    ///
5539    /// In simple terms, it tells you how "tilted" the orbit is.
5540    ///
5541    /// Wikipedia: <https://en.wikipedia.org/wiki/Orbital_inclination>
5542    fn set_inclination(&mut self, inclination: f64);
5543
5544    /// Gets the argument of periapsis of the orbit in radians.
5545    ///
5546    /// Wikipedia:  
5547    /// The argument of periapsis is the angle from the body's
5548    /// ascending node to its periapsis, measured in the direction of
5549    /// motion.  
5550    /// <https://en.wikipedia.org/wiki/Argument_of_periapsis>
5551    ///
5552    /// In simple terms, it tells you how, and in which direction,
5553    /// the orbit "tilts".
5554    #[doc(alias = "get_argument_of_periapsis")]
5555    fn get_arg_pe(&self) -> f64;
5556
5557    /// Sets the argument of periapsis of the orbit in radians.
5558    ///
5559    /// Wikipedia:  
5560    /// The argument of periapsis is the angle from the body's
5561    /// ascending node to its periapsis, measured in the direction of
5562    /// motion.  
5563    /// <https://en.wikipedia.org/wiki/Argument_of_periapsis>
5564    ///
5565    /// In simple terms, it tells you how, and in which direction,
5566    /// the orbit "tilts".
5567    #[doc(alias = "set_argument_of_periapsis")]
5568    fn set_arg_pe(&mut self, arg_pe: f64);
5569
5570    /// Gets the longitude of ascending node of the orbit in radians.
5571    ///
5572    /// Wikipedia:  
5573    /// The longitude of ascending node is the angle from a specified
5574    /// reference direction, called the origin of longitude, to the direction
5575    /// of the ascending node, as measured in a specified reference plane.  
5576    /// <https://en.wikipedia.org/wiki/Longitude_of_the_ascending_node>
5577    ///
5578    /// In simple terms, it tells you how, and in which direction,
5579    /// the orbit "tilts".
5580    #[doc(alias = "get_longitude_of_ascending_node")]
5581    fn get_long_asc_node(&self) -> f64;
5582
5583    /// Sets the longitude of ascending node of the orbit in radians.
5584    ///
5585    /// Wikipedia:  
5586    /// The longitude of ascending node is the angle from a specified
5587    /// reference direction, called the origin of longitude, to the direction
5588    /// of the ascending node, as measured in a specified reference plane.  
5589    /// <https://en.wikipedia.org/wiki/Longitude_of_the_ascending_node>
5590    ///
5591    /// In simple terms, it tells you how, and in which direction,
5592    /// the orbit "tilts".
5593    #[doc(alias = "set_longitude_of_ascending_node")]
5594    fn set_long_asc_node(&mut self, long_asc_node: f64);
5595
5596    /// Gets the mean anomaly of the orbit at a certain epoch.
5597    ///
5598    /// For elliptic orbits, it's measured in radians and so are bounded
5599    /// between 0 and tau; anything out of range will get wrapped around.  
5600    /// For hyperbolic orbits, it's unbounded.
5601    ///
5602    /// Wikipedia:  
5603    /// The mean anomaly at epoch, `M_0`, is defined as the instantaneous mean
5604    /// anomaly at a given epoch, `t_0`.  
5605    /// <https://en.wikipedia.org/wiki/Mean_anomaly#Mean_anomaly_at_epoch>
5606    ///
5607    /// In simple terms, this modifies the "offset" of the orbit progression.
5608    fn get_mean_anomaly_at_epoch(&self) -> f64;
5609
5610    /// Sets the mean anomaly of the orbit at a certain epoch.
5611    ///
5612    /// For elliptic orbits, it's measured in radians and so are bounded
5613    /// between 0 and tau; anything out of range will get wrapped around.  
5614    /// For hyperbolic orbits, it's unbounded.
5615    ///
5616    /// Wikipedia:  
5617    /// The mean anomaly at epoch, `M_0`, is defined as the instantaneous mean
5618    /// anomaly at a given epoch, `t_0`.  
5619    /// <https://en.wikipedia.org/wiki/Mean_anomaly#Mean_anomaly_at_epoch>
5620    ///
5621    /// In simple terms, this modifies the "offset" of the orbit progression.
5622    fn set_mean_anomaly_at_epoch(&mut self, mean_anomaly: f64);
5623
5624    /// Get an initial guess for the hyperbolic eccentric anomaly of an orbit.
5625    ///
5626    /// From the paper:  
5627    /// "A new method for solving the hyperbolic Kepler equation"  
5628    /// by Baisheng Wu et al.  
5629    /// Quote:
5630    /// "we divide the hyperbolic eccentric anomaly interval into two parts:
5631    /// a finite interval and an infinite interval. For the finite interval,
5632    /// we apply a piecewise Pade approximation to establish an initial
5633    /// approximate solution of HKE. For the infinite interval, an analytical
5634    /// initial approximate solution is constructed."
5635    fn get_approx_hyp_ecc_anomaly(&self, mean_anomaly: f64) -> f64 {
5636        let sign = mean_anomaly.signum();
5637        let mean_anomaly = mean_anomaly.abs();
5638        const SINH_5: f64 = 74.20321057778875;
5639
5640        let eccentricity = self.get_eccentricity();
5641
5642        // (Paragraph after Eq. 5 in the aforementioned paper)
5643        //   The [mean anomaly] interval [0, e_c sinh(5) - 5) can
5644        //   be separated into fifteen subintervals corresponding to
5645        //   those intervals of F in [0, 5), see Eq. (4).
5646        sign * if mean_anomaly < eccentricity * SINH_5 - 5.0 {
5647            // We use the Pade approximation of sinh of order
5648            // [3 / 2], in `crate::generated_sinh_approximator`.
5649            // We can then rearrange the equation to a cubic
5650            // equation in terms of (F - a) and solve it.
5651            //
5652            // To quote the paper:
5653            //   Replacing sinh(F) in [the hyperbolic Kepler
5654            //   equation] with its piecewise Pade approximation
5655            //   defined in Eq. (4) [`crate::generated_sinh_approximator`]
5656            //   yields:
5657            //     e_c P(F) - F = M_h                          (6)
5658            //
5659            //   Eq. (6) can be written as a cubic equation in u = F - a, as
5660            //     (e_c p_3 - q_2)u^3 +
5661            //     (e_c p_2 - (M_h + a)q_2 - q_1) u^2 +
5662            //     (e_c p_1 - (M_h + a)q_1 - 1)u +
5663            //     e_c s - M_h - a = 0                         (7)
5664            //
5665            //   Solving Eq. (7) and picking the real root F = F_0 in the
5666            //   corresponding subinterval results in an initial approximate
5667            //   solution to [the hyperbolic Kepler equation].
5668            //
5669            // For context:
5670            // - `e_c` is eccentricity
5671            // - `p_*`, `q_*`, `a`, and `s` is derived from the Pade approximation
5672            //   arguments, which can be retrieved using the
5673            //   `generated_sinh_approximator::get_sinh_approx_params` function
5674            // - `M_h` is the mean anomaly
5675            // - `F` is the eccentric anomaly
5676
5677            use crate::generated_sinh_approximator::get_sinh_approx_params;
5678            let params = get_sinh_approx_params(mean_anomaly);
5679
5680            // We first get the value of each coefficient in the cubic equation:
5681            // Au^3 + Bu^2 + Cu + D = 0
5682            let mean_anom_plus_a = mean_anomaly + params.a;
5683            let coeff_a = eccentricity * params.p_3 - params.q_2;
5684            let coeff_b = eccentricity * params.p_2 - mean_anom_plus_a * params.q_2 - params.q_1;
5685            let coeff_c = eccentricity * params.p_1 - mean_anom_plus_a * params.q_1 - 1.0;
5686            let coeff_d = eccentricity * params.s - mean_anomaly - params.a;
5687
5688            // Then we solve it to get the value of u = F - a
5689            let u = solve_monotone_cubic(coeff_a, coeff_b, coeff_c, coeff_d);
5690
5691            u + params.a
5692        } else {
5693            // Equation 13
5694            // A *very* rough guess, with an error that may exceed 1%.
5695            let rough_guess = (2.0 * mean_anomaly / eccentricity).ln();
5696
5697            /*
5698            A fourth-order Schröder iteration of the second kind
5699            is performed to create a better guess.
5700            ...Apparently it's not a well-known thing, but the aforementioned paper
5701            referenced this other paper about Schröder iterations:
5702            https://doi.org/10.1016/j.cam.2019.02.035
5703
5704            To do the Schröder iteration, we need to compute a delta value
5705            to be added to the rough guess. Part of Equation 15 from the paper is below.
5706
5707            delta = (
5708                    6 * [e_c^2 / (4 * M_h) + F_a] / (e_c * c_a - 1) +
5709                    3 * [e_c * s_a / (e_c * c_a - 1)]{[e_c^2 / (4 * M_h) + F_a] / (e_c * c_a - 1)}^2
5710                ) / (
5711                    6 +
5712                    6 * [e_c * s_a / (e_c * c_a - 1)]{[e_c^2 / (4 * M_h) + F_a] / (e_c * c_a - 1)} +
5713                    [e_c * c_a / (e_c * c_a - 1)]{[e_c^2 / (4 * M_h) + F_a] / (e_c * c_a - 1)}^2
5714                )
5715            ...where:
5716            e_c = eccentricity
5717            F_a = rough guess
5718            c_a = cosh(F_a) = 0.5 * [2 * M_h / e_c + e_c / (2 * M_h)],
5719            s_a = sinh(F_a) = 0.5 * [2 * M_h / e_c - e_c / (2 * M_h)]
5720
5721            Although the equation may look intimidating, there are a lot of repeated values.
5722            We can simplify the equation by extracting the repeated values.
5723
5724            Let:
5725                alpha = e_c^2 / (4 * M_h) + F_a
5726                beta  = 1 / (e_c * c_a - 1)
5727                gamma = alpha * beta
5728
5729            The equation gets simplified into:
5730
5731            delta = (
5732                    6 * gamma +
5733                    3 * e_c * s_a * beta * gamma^2
5734                ) / (
5735                    6 +
5736                    6 * e_c * s_a * beta * gamma +
5737                    e_c * c_a * beta * gamma^2
5738                )
5739
5740            Then we can refine the rough guess into the initial guess:
5741            F_0 = F_a + delta
5742            */
5743
5744            let (c_a, s_a) = {
5745                // c_a and s_a has a lot of repeated values, so we can
5746                // optimize by calculating them together.
5747                // c_a, s_a = 0.5 * [2 * M_h / e_c +- e_c / (2 * M_h)]
5748                //
5749                // define "left"  = 2 * M_h / e_c
5750                // define "right" = e_c / (2 * M_h)
5751
5752                let left = 2.0 * mean_anomaly / eccentricity;
5753                let right = eccentricity / (2.0 * mean_anomaly);
5754
5755                (0.5 * (left + right), 0.5 * (left - right))
5756            };
5757
5758            let alpha = eccentricity * eccentricity / (4.0 * mean_anomaly) + rough_guess;
5759
5760            let beta = (eccentricity * c_a - 1.0).recip();
5761
5762            let gamma = alpha * beta;
5763            let gamma_sq = gamma * gamma;
5764
5765            let delta = (6.0 * alpha * beta + 3.0 * (eccentricity * s_a * beta) * gamma_sq)
5766                / (6.0
5767                    + 6.0 * (eccentricity * s_a * beta) * gamma
5768                    + (eccentricity * c_a * beta) * gamma_sq);
5769
5770            rough_guess + delta
5771        }
5772    }
5773
5774    /// Gets the gravitational parameter of the parent body.
5775    ///
5776    /// The gravitational parameter mu of the parent body equals a certain
5777    /// gravitational constant G times the mass of the parent body M.
5778    ///
5779    /// In other words, mu = GM.
5780    #[doc(alias = "get_mu")]
5781    fn get_gravitational_parameter(&self) -> f64;
5782
5783    /// Sets the gravitational parameter of the parent body.
5784    ///
5785    /// The gravitational parameter mu of the parent body equals a certain
5786    /// gravitational constant G times the mass of the parent body M.
5787    ///
5788    /// In other words, mu = GM.
5789    ///
5790    /// # Example
5791    /// ```
5792    /// use keplerian_sim::{Orbit, OrbitTrait, MuSetterMode};
5793    ///
5794    /// let mut orbit = Orbit::new(
5795    ///     0.0, // Eccentricity
5796    ///     1.0, // Periapsis
5797    ///     0.0, // Inclination
5798    ///     0.0, // Argument of Periapsis
5799    ///     0.0, // Longitude of Ascending Node
5800    ///     0.0, // Mean anomaly at epoch
5801    ///     1.0, // Gravitational parameter (mu = GM)
5802    /// );
5803    ///
5804    /// orbit.set_gravitational_parameter(3.0, MuSetterMode::KeepElements);
5805    ///
5806    /// assert_eq!(orbit.get_eccentricity(), 0.0);
5807    /// assert_eq!(orbit.get_periapsis(), 1.0);
5808    /// assert_eq!(orbit.get_inclination(), 0.0);
5809    /// assert_eq!(orbit.get_arg_pe(), 0.0);
5810    /// assert_eq!(orbit.get_long_asc_node(), 0.0);
5811    /// assert_eq!(orbit.get_mean_anomaly_at_epoch(), 0.0);
5812    /// assert_eq!(orbit.get_gravitational_parameter(), 3.0);
5813    /// ```
5814    #[doc(alias = "set_mu")]
5815    fn set_gravitational_parameter(&mut self, gravitational_parameter: f64, mode: MuSetterMode);
5816
5817    /// Gets the time it takes to complete one revolution of the orbit.
5818    ///
5819    /// This function returns infinite values for parabolic trajectories and
5820    /// NaN for hyperbolic trajectories.
5821    ///
5822    /// # Time
5823    /// The time is measured in seconds.
5824    ///
5825    /// # Performance
5826    /// This function is performant and is unlikely to be the cause of any
5827    /// performance issues.
5828    fn get_orbital_period(&self) -> f64 {
5829        // T = 2pi * sqrt(a^3 / GM)
5830        // https://en.wikipedia.org/wiki/Orbital_period
5831        TAU * (self.get_semi_major_axis().powi(3) / self.get_gravitational_parameter()).sqrt()
5832    }
5833}
5834
5835/// A mode to describe how the gravitational parameter setter should behave.
5836///
5837/// This is used to describe how the setter should behave when setting the
5838/// gravitational parameter of the parent body.
5839///
5840/// # Which mode should I use?
5841/// The mode you should use depends on what you expect from setting the mu value
5842/// to a different value.
5843///
5844/// If you just want to set the mu value naïvely (without touching the
5845/// other orbital elements), you can use the `KeepElements` variant.
5846///
5847/// If this is part of a simulation and you want to keep the current position
5848/// (not caring about the velocity), you can use the `KeepPositionAtTime` variant.
5849///
5850/// If you want to keep the current position and velocity, you can use either
5851/// the `KeepKnownStateVectors` or `KeepStateVectorsAtTime` modes, the former
5852/// being more performant if you already know the state vectors beforehand.
5853#[derive(Debug, Clone, Copy, PartialEq)]
5854pub enum MuSetterMode {
5855    /// Keep all the other orbital parameters the same.
5856    ///
5857    /// **This will change the position and velocity of the orbiting body abruptly,
5858    /// if you use the time-based functions.** It will not, however, change the trajectory
5859    /// of the orbit.
5860    ///
5861    /// # Performance
5862    /// This mode is the fastest of the mu setter modes as it is simply an
5863    /// assignment operation.
5864    ///
5865    /// # Example
5866    /// ```
5867    /// use keplerian_sim::{Orbit, OrbitTrait, MuSetterMode};
5868    ///
5869    /// let mut orbit = Orbit::new(
5870    ///     0.0, // Eccentricity
5871    ///     1.0, // Periapsis
5872    ///     0.0, // Inclination
5873    ///     0.0, // Argument of Periapsis
5874    ///     0.0, // Longitude of Ascending Node
5875    ///     0.0, // Mean anomaly at epoch
5876    ///     1.0, // Gravitational parameter (mu = GM)
5877    /// );
5878    ///
5879    /// orbit.set_gravitational_parameter(3.0, MuSetterMode::KeepElements);
5880    ///
5881    /// assert_eq!(orbit.get_eccentricity(), 0.0);
5882    /// assert_eq!(orbit.get_periapsis(), 1.0);
5883    /// assert_eq!(orbit.get_inclination(), 0.0);
5884    /// assert_eq!(orbit.get_arg_pe(), 0.0);
5885    /// assert_eq!(orbit.get_long_asc_node(), 0.0);
5886    /// assert_eq!(orbit.get_mean_anomaly_at_epoch(), 0.0);
5887    /// assert_eq!(orbit.get_gravitational_parameter(), 3.0);
5888    /// ```
5889    KeepElements,
5890    /// Keep the overall shape of the orbit, but modify the mean anomaly at epoch
5891    /// such that the position at the given time is the same.
5892    ///
5893    /// **This will change the velocity of the orbiting body abruptly, if you use
5894    /// the time-based position/velocity getter functions.**
5895    ///
5896    /// # Time
5897    /// The time is measured in seconds.
5898    ///
5899    /// # Performance
5900    /// This mode is slower than the `KeepElements` mode as it has to compute a new
5901    /// mean anomaly at epoch. However, this isn't too expensive and only costs a
5902    /// few squareroot operations.
5903    ///
5904    /// # Example
5905    /// ```
5906    /// use keplerian_sim::{Orbit, OrbitTrait, MuSetterMode};
5907    ///
5908    /// let mut orbit = Orbit::new(
5909    ///     0.0, // Eccentricity
5910    ///     1.0, // Periapsis
5911    ///     0.0, // Inclination
5912    ///     0.0, // Argument of Periapsis
5913    ///     0.0, // Longitude of Ascending Node
5914    ///     0.0, // Mean anomaly at epoch
5915    ///     1.0, // Gravitational parameter (mu = GM)
5916    /// );
5917    ///
5918    /// orbit.set_gravitational_parameter(
5919    ///     3.0,
5920    ///     MuSetterMode::KeepPositionAtTime(0.4),
5921    /// );
5922    ///
5923    /// assert_eq!(orbit.get_eccentricity(), 0.0);
5924    /// assert_eq!(orbit.get_periapsis(), 1.0);
5925    /// assert_eq!(orbit.get_inclination(), 0.0);
5926    /// assert_eq!(orbit.get_arg_pe(), 0.0);
5927    /// assert_eq!(orbit.get_long_asc_node(), 0.0);
5928    /// assert_eq!(orbit.get_mean_anomaly_at_epoch(), -0.2928203230275509);
5929    /// assert_eq!(orbit.get_gravitational_parameter(), 3.0);
5930    /// ```
5931    KeepPositionAtTime(f64),
5932    /// Keep the position and velocity of the orbit at a certain time
5933    /// roughly unchanged, using known [StateVectors] to avoid
5934    /// duplicate calculations.
5935    ///
5936    /// **This will change the orbit's overall trajectory.**
5937    ///
5938    /// # Time
5939    /// The time is measured in seconds.
5940    ///
5941    /// # Unchecked Operation
5942    /// This mode does not check whether or not the state vectors and time values given
5943    /// match up. Mismatched values may result in undesired behavior and NaNs.  
5944    /// Use the [`KeepStateVectorsAtTime`][MuSetterMode::KeepStateVectorsAtTime]
5945    /// mode if you don't want this unchecked operation.
5946    ///
5947    /// # Performance
5948    /// This mode uses some trigonometry, and therefore is not very performant.  
5949    /// Consider using another mode if performance is an issue.  
5950    ///
5951    /// This is, however, significantly more performant than the numerical approach
5952    /// used in the [`KeepStateVectorsAtTime`][MuSetterMode::KeepStateVectorsAtTime]
5953    /// mode.
5954    ///
5955    /// # Example
5956    /// ```
5957    /// use keplerian_sim::{Orbit, OrbitTrait, MuSetterMode};
5958    ///
5959    /// let mut orbit = Orbit::new(
5960    ///     0.0, // Eccentricity
5961    ///     1.0, // Periapsis
5962    ///     0.0, // Inclination
5963    ///     0.0, // Argument of Periapsis
5964    ///     0.0, // Longitude of Ascending Node
5965    ///     0.0, // Mean anomaly at epoch
5966    ///     1.0, // Gravitational parameter (mu = GM)
5967    /// );
5968    ///
5969    /// let time = 0.75;
5970    ///
5971    /// let state_vectors = orbit.get_state_vectors_at_time(time);
5972    ///
5973    /// orbit.set_gravitational_parameter(
5974    ///     3.0,
5975    ///     MuSetterMode::KeepKnownStateVectors {
5976    ///         state_vectors,
5977    ///         time
5978    ///     }
5979    /// );
5980    ///
5981    /// let new_state_vectors = orbit.get_state_vectors_at_time(time);
5982    ///
5983    /// println!("Old state vectors: {state_vectors:?}");
5984    /// println!("New state vectors: {new_state_vectors:?}");
5985    /// ```
5986    KeepKnownStateVectors {
5987        /// The state vectors describing the point in the orbit where you want the
5988        /// position and velocity to remain the same.
5989        ///
5990        /// Must correspond to the time value, or undesired behavior and NaNs may occur.
5991        state_vectors: StateVectors,
5992
5993        /// The time value of the point in the orbit
5994        /// where you want the position and velocity to remain the same.
5995        ///
5996        /// The time is measured in seconds.
5997        ///
5998        /// Must correspond to the given state vectors, or undesired behavior and NaNs may occur.
5999        time: f64,
6000    },
6001    /// Keep the position and velocity of the orbit at a certain time
6002    /// roughly unchanged.
6003    ///
6004    /// **This will change the orbit's overall trajectory.**
6005    ///
6006    /// # Time
6007    /// The time is measured in seconds.
6008    ///
6009    /// # Performance
6010    /// This mode uses numerical approach methods, and therefore is not performant.  
6011    /// Consider using another mode if performance is an issue.  
6012    ///
6013    /// Alternatively, if you already know the state vectors (position and velocity)
6014    /// of the point you want to keep, use the
6015    /// [`KeepKnownStateVectors`][MuSetterMode::KeepKnownStateVectors]
6016    /// mode instead. This skips the numerical method used to obtain the eccentric anomaly
6017    /// and some more trigonometry.
6018    ///
6019    /// If you only know the eccentric anomaly and true anomaly, it's more performant
6020    /// to derive state vectors from those first and then use the aforementioned
6021    /// [`KeepKnownStateVectors`][MuSetterMode::KeepKnownStateVectors] mode. This can
6022    /// be done using the [`Orbit::get_state_vectors_at_eccentric_anomaly`] function, for
6023    /// example.
6024    ///
6025    /// # Example
6026    /// ```
6027    /// use keplerian_sim::{Orbit, OrbitTrait, MuSetterMode};
6028    ///
6029    /// let old_orbit = Orbit::new(
6030    ///     0.0, // Eccentricity
6031    ///     1.0, // Periapsis
6032    ///     0.0, // Inclination
6033    ///     0.0, // Argument of Periapsis
6034    ///     0.0, // Longitude of Ascending Node
6035    ///     0.0, // Mean anomaly at epoch
6036    ///     1.0, // Gravitational parameter (mu = GM)
6037    /// );
6038    ///
6039    /// let mut new_orbit = old_orbit.clone();
6040    ///
6041    /// const TIME: f64 = 1.5;
6042    ///
6043    /// new_orbit.set_gravitational_parameter(
6044    ///     3.0,
6045    ///     MuSetterMode::KeepStateVectorsAtTime(TIME)
6046    /// );
6047    ///
6048    /// let old_state_vectors = old_orbit.get_state_vectors_at_time(TIME);
6049    /// let new_state_vectors = new_orbit.get_state_vectors_at_time(TIME);
6050    ///
6051    /// println!("Old state vectors: {old_state_vectors:?}");
6052    /// println!("New state vectors: {new_state_vectors:?}");
6053    /// ```
6054    KeepStateVectorsAtTime(f64),
6055}
6056
6057/// An error to describe why setting the periapsis of an orbit failed.
6058#[derive(PartialEq, Eq, Debug, Clone, Copy)]
6059pub enum ApoapsisSetterError {
6060    /// ### Attempt to set apoapsis to a value less than periapsis.
6061    /// By definition, an orbit's apoapsis is the highest point in the orbit,
6062    /// and its periapsis is the lowest point in the orbit.  
6063    /// Therefore, it doesn't make sense for the apoapsis to be lower than the periapsis.
6064    ApoapsisLessThanPeriapsis,
6065
6066    /// ### Attempt to set apoapsis to a negative value.
6067    /// By definition, the apoapsis is the highest point in the orbit.  
6068    /// You can't be a negative distance away from the center of mass of the parent body.  
6069    /// Therefore, it doesn't make sense for the apoapsis to be lower than zero.
6070    ApoapsisNegative,
6071}
6072
6073#[cfg(test)]
6074mod tests;
6075
6076#[inline]
6077fn keplers_equation(mean_anomaly: f64, eccentric_anomaly: f64, eccentricity: f64) -> f64 {
6078    eccentric_anomaly - (eccentricity * eccentric_anomaly.sin()) - mean_anomaly
6079}
6080#[inline]
6081fn keplers_equation_derivative(eccentric_anomaly: f64, eccentricity: f64) -> f64 {
6082    1.0 - (eccentricity * eccentric_anomaly.cos())
6083}
6084#[inline]
6085fn keplers_equation_second_derivative(eccentric_anomaly: f64, eccentricity: f64) -> f64 {
6086    eccentricity * eccentric_anomaly.sin()
6087}
6088
6089/// Get the hyperbolic sine and cosine of a number.
6090///
6091/// Usually faster than calling `x.sinh()` and `x.cosh()` separately.
6092///
6093/// Returns a tuple which contains:
6094/// - 0: The hyperbolic sine of the number.
6095/// - 1: The hyperbolic cosine of the number.
6096pub fn sinhcosh(x: f64) -> (f64, f64) {
6097    let e_x = x.exp();
6098    let e_neg_x = e_x.recip();
6099
6100    ((e_x - e_neg_x) * 0.5, (e_x + e_neg_x) * 0.5)
6101}
6102
6103/// Solve a cubic equation to get its real root.
6104///
6105/// The cubic equation is in the form of:
6106/// ax^3 + bx^2 + cx + d
6107///
6108/// The cubic equation is assumed to be monotone.  
6109/// If it isn't monotone (i.e., the discriminant
6110/// is negative), it may return an incorrect value
6111/// or NaN.
6112///
6113/// # Performance
6114/// This function involves several divisions,
6115/// squareroots, and cuberoots, and therefore is not
6116/// very performant. It is recommended to cache this value if you can.
6117fn solve_monotone_cubic(a: f64, b: f64, c: f64, d: f64) -> f64 {
6118    // Normalize coefficients so that a = 1
6119    // ax^3 + bx^2 + cx + d
6120    // ...where b, c, d are the normalized coefficients,
6121    // and a = 1
6122    let b = b / a;
6123    let c = c / a;
6124    let d = d / a;
6125
6126    // Depress the cubic equation
6127    // t^3 + pt + q = 0
6128    // ...where:
6129    // p = (3ac - b^2) / (3a^2)
6130    // q = (2b^3 - 9abc + 27da^2) / (27a^3)
6131    // ...since a = 1, we can simplify them to:
6132    // p = (3c - b^2) / 3
6133    // q = (2b^3 - 9bc + 27d) / 27
6134    let b_sq = b * b;
6135
6136    let p = (3.0 * c - b_sq) / 3.0;
6137    let q = (2.0 * b_sq * b - 9.0 * b * c + 27.0 * d) / 27.0;
6138
6139    let q_div_two = q / 2.0;
6140    let p_div_three = p / 3.0;
6141    let p_div_three_cubed = p_div_three * p_div_three * p_div_three;
6142    let discriminant = q_div_two * q_div_two + p_div_three_cubed;
6143
6144    if discriminant < 0.0 {
6145        // Function is not monotone
6146        return f64::NAN;
6147    }
6148
6149    let t = {
6150        let sqrt_discriminant = discriminant.sqrt();
6151        let neg_q_div_two = -q_div_two;
6152        let u = (neg_q_div_two + sqrt_discriminant).cbrt();
6153        let v = (neg_q_div_two - sqrt_discriminant).cbrt();
6154        u + v
6155    };
6156
6157    // x_i = t_i - b / 3a
6158    // here, a = 1
6159    t - b / 3.0
6160}
6161
6162mod generated_sinh_approximator;