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(¬_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;