Skip to main content

nyx_space/
utils.rs

1/*
2    Nyx, blazing fast astrodynamics
3    Copyright (C) 2018-onwards Christopher Rabotin <christopher.rabotin@gmail.com>
4
5    This program is free software: you can redistribute it and/or modify
6    it under the terms of the GNU Affero General Public License as published
7    by the Free Software Foundation, either version 3 of the License, or
8    (at your option) any later version.
9
10    This program is distributed in the hope that it will be useful,
11    but WITHOUT ANY WARRANTY; without even the implied warranty of
12    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13    GNU Affero General Public License for more details.
14
15    You should have received a copy of the GNU Affero General Public License
16    along with this program.  If not, see <https://www.gnu.org/licenses/>.
17*/
18
19use crate::cosmic::Orbit;
20use crate::linalg::{
21    DefaultAllocator, DimName, Matrix3, OVector, Vector3, Vector6, allocator::Allocator,
22};
23use nalgebra::Complex;
24
25/// Returns the skew-symmetric matrix (also known as the tilde matrix)
26/// corresponding to the provided 3D vector.
27///
28/// The skew-symmetric matrix of a vector `v` is defined as:
29///
30/// ```plaintext
31///  0   -v.z  v.y
32///  v.z  0   -v.x
33/// -v.y  v.x  0
34/// ```
35///
36/// This matrix has the property that for any vector `w`, the cross product `v x w`
37/// can be computed as the matrix product of the skew-symmetric matrix of `v` and `w`.
38pub fn tilde_matrix(v: &Vector3<f64>) -> Matrix3<f64> {
39    Matrix3::new(0.0, -v.z, v.y, v.z, 0.0, -v.x, -v.y, v.x, 0.0)
40}
41
42/// Checks if the provided 3x3 matrix is diagonal.
43///
44/// A square matrix is considered diagonal if all its off-diagonal elements are zero.
45/// This function verifies this property for a given 3x3 matrix.
46/// It checks each off-diagonal element of the matrix and returns `false` if any of them
47/// is not approximately zero, considering a tolerance defined by `f64::EPSILON`.
48///
49/// # Arguments
50///
51/// * `m` - A 3x3 matrix of `f64` elements to be checked.
52///
53/// # Returns
54///
55/// * `bool` - Returns `true` if the matrix is diagonal, `false` otherwise.
56///
57/// # Example
58///
59/// ```
60/// use nyx_space::utils::is_diagonal;
61/// use nyx_space::linalg::Matrix3;
62///
63/// let m = Matrix3::new(1.0, 0.0, 0.0,
64///                      0.0, 2.0, 0.0,
65///                      0.0, 0.0, 3.0);
66/// assert_eq!(is_diagonal(&m), true);
67/// ```
68///
69/// # Note
70///
71/// This function uses `f64::EPSILON` as the tolerance for checking if an element is approximately zero.
72/// This means that elements with absolute value less than `f64::EPSILON` are considered zero.
73pub fn is_diagonal(m: &Matrix3<f64>) -> bool {
74    for i in 0..3 {
75        for j in 0..3 {
76            if i != j && m[(i, j)].abs() > f64::EPSILON {
77                return false;
78            }
79        }
80    }
81    true
82}
83
84/// Checks if the given matrix represents a stable linear system by examining its eigenvalues.
85///
86/// Stability of a linear system is determined by the properties of its eigenvalues:
87/// - If any eigenvalue has a positive real part, the system is unstable.
88/// - If the real part of an eigenvalue is zero and the imaginary part is non-zero, the system is oscillatory.
89/// - If the real part of an eigenvalue is negative, the system tends towards stability.
90/// - If both the real and imaginary parts of an eigenvalue are zero, the system is invariant.
91///
92/// # Arguments
93///
94/// `eigenvalues` - A vector of complex numbers representing the eigenvalues of the system.
95///
96/// # Returns
97///
98/// `bool` - Returns `true` if the system is stable, `false` otherwise.
99///
100/// # Example
101///
102/// ```
103/// use nyx_space::utils::are_eigenvalues_stable;
104/// use nyx_space::linalg::Vector2;
105/// use nalgebra::Complex;
106///
107/// let eigenvalues = Vector2::new(Complex::new(-1.0, 0.0), Complex::new(0.0, 1.0));
108/// assert_eq!(are_eigenvalues_stable(&eigenvalues), true);
109/// ```
110/// # Source
111///
112/// [Chemical Process Dynamics and Controls (Woolf)](https://eng.libretexts.org/Bookshelves/Industrial_and_Systems_Engineering/Book%3A_Chemical_Process_Dynamics_and_Controls_(Woolf)/10%3A_Dynamical_Systems_Analysis/10.04%3A_Using_eigenvalues_and_eigenvectors_to_find_stability_and_solve_ODEs#Summary_of_Eigenvalue_Graphs)
113pub fn are_eigenvalues_stable<N: DimName>(eigenvalues: &OVector<Complex<f64>, N>) -> bool
114where
115    DefaultAllocator: Allocator<N>,
116{
117    eigenvalues.iter().all(|ev| ev.re <= 0.0)
118}
119
120/// Returns the provided angle bounded between 0.0 and 360.0.
121///
122/// This function takes an angle (in degrees) and normalizes it to the range [0, 360).
123/// If the angle is negative, it will be converted to a positive angle in the equivalent position.
124/// For example, an angle of -90 degrees will be converted to 270 degrees.
125///
126/// # Arguments
127///
128/// * `angle` - An angle in degrees.
129///
130pub fn between_0_360(angle: f64) -> f64 {
131    let mut bounded = angle % 360.0;
132    if bounded < 0.0 {
133        bounded += 360.0;
134    }
135    bounded
136}
137
138/// Returns the provided angle bounded between -180.0 and +180.0
139pub fn between_pm_180(angle: f64) -> f64 {
140    between_pm_x(angle, 180.0)
141}
142
143/// Returns the provided angle bounded between -x and +x.
144///
145/// This function takes an angle (in degrees) and normalizes it to the range [-x, x).
146/// If the angle is outside this range, it will be converted to an equivalent angle within this range.
147/// For example, if x is 180, an angle of 270 degrees will be converted to -90 degrees.
148///
149/// # Arguments
150///
151/// * `angle` - An angle in degrees.
152/// * `x` - The boundary for the angle normalization.
153pub fn between_pm_x(angle: f64, x: f64) -> f64 {
154    let mut bounded = angle % (2.0 * x);
155    if bounded > x {
156        bounded -= 2.0 * x;
157    }
158    if bounded < -x {
159        bounded += 2.0 * x;
160    }
161    bounded
162}
163
164/// The Kronecker delta function
165pub fn kronecker(a: f64, b: f64) -> f64 {
166    if (a - b).abs() <= f64::EPSILON {
167        1.0
168    } else {
169        0.0
170    }
171}
172
173/// Returns a rotation matrix for a rotation about the X axis.
174///
175/// # Arguments
176///
177/// * `angle_rad` - The angle of rotation in radians.
178///
179/// # Warning
180///
181/// This function returns a matrix for a COORDINATE SYSTEM rotation by `angle_rad` radians.
182/// When this matrix is applied to a vector, it rotates the vector by `-angle_rad` radians, not `angle_rad` radians.
183/// Applying the matrix to a vector yields the vector's representation relative to the rotated coordinate system.
184///
185/// # Example
186///
187/// ```
188/// use nyx_space::utils::r1;
189///
190/// let angle_rad = std::f64::consts::PI / 2.0;
191/// let rotation_matrix = r1(angle_rad);
192/// ```
193///
194/// # Source
195///
196/// [NAIF SPICE Toolkit](https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/cspice/eul2xf_c.html)
197pub fn r1(angle_rad: f64) -> Matrix3<f64> {
198    let (s, c) = angle_rad.sin_cos();
199    Matrix3::new(1.0, 0.0, 0.0, 0.0, c, s, 0.0, -s, c)
200}
201
202/// Returns a rotation matrix for a rotation about the Y axis.
203///
204/// # Arguments
205///
206/// * `angle` - The angle of rotation in radians.
207///
208/// # Warning
209///
210/// This function returns a matrix for a COORDINATE SYSTEM rotation by `angle_rad` radians.
211/// When this matrix is applied to a vector, it rotates the vector by `-angle_rad` radians, not `angle_rad` radians.
212/// Applying the matrix to a vector yields the vector's representation relative to the rotated coordinate system.
213///
214/// # Example
215///
216/// ```
217/// use nyx_space::utils::r2;
218///
219/// let angle_rad = std::f64::consts::PI / 2.0;
220/// let rotation_matrix = r2(angle_rad);
221/// ```
222///
223/// # Source
224///
225/// [NAIF SPICE Toolkit](https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/cspice/eul2xf_c.html)
226pub fn r2(angle_rad: f64) -> Matrix3<f64> {
227    let (s, c) = angle_rad.sin_cos();
228    Matrix3::new(c, 0.0, -s, 0.0, 1.0, 0.0, s, 0.0, c)
229}
230
231/// Returns a rotation matrix for a rotation about the Z axis.
232///
233/// # Arguments
234///
235/// * `angle_rad` - The angle of rotation in radians.
236///
237/// # Warning
238///
239/// This function returns a matrix for a COORDINATE SYSTEM rotation by `angle_rad` radians.
240/// When this matrix is applied to a vector, it rotates the vector by `-angle_rad` radians, not `angle_rad` radians.
241/// Applying the matrix to a vector yields the vector's representation relative to the rotated coordinate system.
242///
243/// # Example
244///
245/// ```
246/// use nyx_space::utils::r3;
247///
248/// let angle_rad = std::f64::consts::PI / 2.0;
249/// let rotation_matrix = r3(angle_rad);
250/// ```
251///
252/// # Source
253///
254/// [NAIF SPICE Toolkit](https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/cspice/eul2xf_c.html)
255pub fn r3(angle_rad: f64) -> Matrix3<f64> {
256    let (s, c) = angle_rad.sin_cos();
257    Matrix3::new(c, s, 0.0, -s, c, 0.0, 0.0, 0.0, 1.0)
258}
259
260/// Rotate a vector about a given axis
261///
262/// # Arguments
263///
264/// * `v` - A vector to be rotated.
265/// * `axis` - The axis around which to rotate the vector.
266/// * `theta` - The angle by which to rotate the vector.
267///
268/// # Returns
269///
270/// A new vector that is the result of rotating `v` around `axis` by `theta` radians.
271pub fn rotv(v: &Vector3<f64>, axis: &Vector3<f64>, theta: f64) -> Vector3<f64> {
272    let k_hat = axis.normalize();
273    v.scale(theta.cos())
274        + k_hat.cross(v).scale(theta.sin())
275        + k_hat.scale(k_hat.dot(v) * (1.0 - theta.cos()))
276}
277
278/// Returns the components of vector a orthogonal to b
279///
280/// # Arguments
281///
282/// * `a` - The vector whose orthogonal components are to be calculated.
283/// * `b` - The vector to which `a` is to be made orthogonal.
284///
285/// # Returns
286///
287/// A new vector that is the orthogonal projection of `a` onto `b`.
288pub fn perpv(a: &Vector3<f64>, b: &Vector3<f64>) -> Vector3<f64> {
289    let big_a = a.amax();
290    let big_b = b.amax();
291    if big_a < f64::EPSILON {
292        Vector3::zeros()
293    } else if big_b < f64::EPSILON {
294        *a
295    } else {
296        let a_scl = a / big_a;
297        let b_scl = b / big_b;
298        let v = projv(&a_scl, &b_scl);
299        (a_scl - v) * big_a
300    }
301}
302
303/// Returns the projection of a onto b
304///
305/// # Arguments
306///
307/// * `a` - The vector to be projected.
308/// * `b` - The vector onto which `a` is to be projected.
309///
310/// # Returns
311///
312/// * A new vector that is the projection of `a` onto `b`.
313pub fn projv(a: &Vector3<f64>, b: &Vector3<f64>) -> Vector3<f64> {
314    let b_norm_squared = b.norm_squared();
315    if b_norm_squared.abs() < f64::EPSILON {
316        Vector3::zeros()
317    } else {
318        b.scale(a.dot(b) / b_norm_squared)
319    }
320}
321
322/// Computes the Root Sum Squared (RSS) state errors between two provided vectors.
323///
324/// # Arguments
325///
326/// * `prop_err` - A vector representing the propagated error.
327/// * `cur_state` - A vector representing the current state.
328///
329/// # Returns
330///
331/// A f64 value representing the RSS state error.
332pub fn rss_errors<N: DimName>(prop_err: &OVector<f64, N>, cur_state: &OVector<f64, N>) -> f64
333where
334    DefaultAllocator: Allocator<N>,
335{
336    prop_err
337        .iter()
338        .zip(cur_state.iter())
339        .map(|(&x, &y)| (x - y).powi(2))
340        .sum::<f64>()
341        .sqrt()
342}
343
344/// Computes the Root Sum Squared (RSS) orbit errors in kilometers and kilometers per second.
345///
346/// # Arguments
347///
348/// * `prop_err` - An Orbit instance representing the propagated error.
349/// * `cur_state` - An Orbit instance representing the current state.
350///
351/// # Returns
352///
353/// A tuple of f64 values representing the RSS orbit errors in radius and velocity.
354pub fn rss_orbit_errors(prop_err: &Orbit, cur_state: &Orbit) -> (f64, f64) {
355    (
356        rss_errors(&prop_err.radius_km, &cur_state.radius_km),
357        rss_errors(&prop_err.velocity_km_s, &cur_state.velocity_km_s),
358    )
359}
360
361/// Computes the Root Sum Squared (RSS) state errors in position and in velocity of two orbit vectors [P V].
362///
363/// # Arguments
364///
365/// * `prop_err` - A Vector6 instance representing the propagated error.
366/// * `cur_state` - A Vector6 instance representing the current state.
367///
368/// # Returns
369///
370/// A tuple of f64 values representing the RSS orbit vector errors in radius and velocity.
371pub fn rss_orbit_vec_errors(prop_err: &Vector6<f64>, cur_state: &Vector6<f64>) -> (f64, f64) {
372    let err_radius = (prop_err.fixed_rows::<3>(0) - cur_state.fixed_rows::<3>(0)).norm();
373    let err_velocity = (prop_err.fixed_rows::<3>(3) - cur_state.fixed_rows::<3>(3)).norm();
374    (err_radius, err_velocity)
375}
376
377/// Normalize a value between -1.0 and 1.0
378///
379/// # Arguments
380///
381/// * `x` - The value to be normalized.
382/// * `min_x` - The minimum value in the range of `x`.
383/// * `max_x` - The maximum value in the range of `x`.
384///
385/// # Returns
386///
387/// A normalized value between -1.0 and 1.0.
388pub fn normalize(x: f64, min_x: f64, max_x: f64) -> f64 {
389    2.0 * (x - min_x) / (max_x - min_x) - 1.0
390}
391
392/// Denormalize a value between -1.0 and 1.0
393///
394/// # Arguments
395///
396/// * `xp` - The value to be denormalized.
397/// * `min_x` - The minimum value in the original range.
398/// * `max_x` - The maximum value in the original range.
399///
400/// # Returns
401///
402/// A denormalized value between `min_x` and `max_x`.
403pub fn denormalize(xp: f64, min_x: f64, max_x: f64) -> f64 {
404    (max_x - min_x) * (xp + 1.0) / 2.0 + min_x
405}
406
407/// Capitalize the first letter of a string
408///
409/// # Arguments
410///
411/// `s` - The string to be capitalized.
412///
413/// # Returns
414///
415/// A new string with the first letter capitalized.
416///
417/// # Source
418///
419/// <https://stackoverflow.com/questions/38406793/why-is-capitalizing-the-first-letter-of-a-string-so-convoluted-in-rust>
420pub fn capitalize(s: &str) -> String {
421    let mut c = s.chars();
422    match c.next() {
423        None => String::new(),
424        Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
425    }
426}
427
428#[macro_export]
429macro_rules! pseudo_inverse {
430    ($mat:expr) => {{
431        use $crate::md::TargetingError;
432        let (rows, cols) = $mat.shape();
433        if rows < cols {
434            match ($mat * $mat.transpose()).try_inverse() {
435                Some(m1_inv) => Ok($mat.transpose() * m1_inv),
436                None => Err(TargetingError::SingularJacobian),
437            }
438        } else {
439            match ($mat.transpose() * $mat).try_inverse() {
440                Some(m2_inv) => Ok(m2_inv * $mat.transpose()),
441                None => Err(TargetingError::SingularJacobian),
442            }
443        }
444    }};
445}
446
447/// Returns the order of mangitude of the provided value
448/// ```
449/// use nyx_space::utils::mag_order;
450/// assert_eq!(mag_order(1000.0), 3);
451/// assert_eq!(mag_order(-5000.0), 3);
452/// assert_eq!(mag_order(-0.0005), -4);
453/// ```
454pub fn mag_order(value: f64) -> i32 {
455    value.abs().log10().floor() as i32
456}
457
458/// Returns the unit vector of the moved input vector
459pub fn unitize(v: Vector3<f64>) -> Vector3<f64> {
460    if v.norm() < f64::EPSILON {
461        v
462    } else {
463        v / v.norm()
464    }
465}
466
467/// Converts the input vector V from Cartesian coordinates to spherical coordinates
468/// Returns ρ, θ, φ where the range ρ is in the units of the input vector and the angles are in radians
469pub fn cartesian_to_spherical(v: &Vector3<f64>) -> (f64, f64, f64) {
470    if v.norm() < f64::EPSILON {
471        (0.0, 0.0, 0.0)
472    } else {
473        let range_ρ = v.norm();
474        let θ = v.y.atan2(v.x);
475        let φ = (v.z / range_ρ).acos();
476        (range_ρ, θ, φ)
477    }
478}
479
480/// Converts the input vector V from Cartesian coordinates to spherical coordinates
481/// Returns ρ, θ, φ where the range ρ is in the units of the input vector and the angles are in radians
482#[allow(clippy::many_single_char_names)]
483pub fn spherical_to_cartesian(range_ρ: f64, θ: f64, φ: f64) -> Vector3<f64> {
484    if range_ρ < f64::EPSILON {
485        // Treat a negative range as a zero vector
486        Vector3::zeros()
487    } else {
488        let x = range_ρ * φ.sin() * θ.cos();
489        let y = range_ρ * φ.sin() * θ.sin();
490        let z = range_ρ * φ.cos();
491        Vector3::new(x, y, z)
492    }
493}
494
495#[cfg(test)]
496mod tests {
497    use super::*;
498    use approx::assert_abs_diff_eq;
499    use nalgebra::{Complex, OVector};
500
501    #[test]
502    fn test_stable_eigenvalues() {
503        let eigenvalues = OVector::<Complex<f64>, nalgebra::U2>::from_column_slice(&[
504            Complex::new(-1.0, 0.0),
505            Complex::new(0.0, 0.0),
506        ]);
507        assert!(are_eigenvalues_stable(&eigenvalues));
508    }
509
510    #[test]
511    fn test_unstable_eigenvalues() {
512        let eigenvalues = OVector::<Complex<f64>, nalgebra::U2>::from_column_slice(&[
513            Complex::new(1.0, 0.0),
514            Complex::new(0.0, 0.0),
515        ]);
516        assert!(!are_eigenvalues_stable(&eigenvalues));
517    }
518
519    #[test]
520    fn test_oscillatory_eigenvalues() {
521        let eigenvalues = OVector::<Complex<f64>, nalgebra::U2>::from_column_slice(&[
522            Complex::new(0.0, 1.0),
523            Complex::new(0.0, -1.0),
524        ]);
525        assert!(are_eigenvalues_stable(&eigenvalues));
526    }
527
528    #[test]
529    fn test_invariant_eigenvalues() {
530        let eigenvalues =
531            OVector::<Complex<f64>, nalgebra::U1>::from_column_slice(&[Complex::new(0.0, 0.0)]);
532        assert!(are_eigenvalues_stable(&eigenvalues));
533    }
534
535    #[test]
536    fn test_angle_bounds() {
537        approx::assert_relative_eq!(between_pm_180(181.0), -179.0);
538        approx::assert_relative_eq!(between_0_360(-179.0), 181.0);
539    }
540
541    #[test]
542    fn test_positive_angle() {
543        approx::assert_relative_eq!(between_0_360(450.0), 90.0);
544        approx::assert_relative_eq!(between_pm_x(270.0, 180.0), -90.0);
545    }
546
547    #[test]
548    fn test_negative_angle() {
549        approx::assert_relative_eq!(between_0_360(-90.0), 270.0);
550        approx::assert_relative_eq!(between_pm_x(-270.0, 180.0), 90.0);
551    }
552
553    #[test]
554    fn test_angle_in_range() {
555        approx::assert_relative_eq!(between_0_360(180.0), 180.0);
556        approx::assert_relative_eq!(between_pm_x(90.0, 180.0), 90.0);
557    }
558
559    #[test]
560    fn test_zero_angle() {
561        approx::assert_relative_eq!(between_0_360(0.0), 0.0);
562        approx::assert_relative_eq!(between_pm_x(0.0, 180.0), 0.0);
563    }
564
565    #[test]
566    fn test_full_circle_angle() {
567        approx::assert_relative_eq!(between_0_360(360.0), 0.0);
568        approx::assert_relative_eq!(between_pm_x(360.0, 180.0), 0.0);
569    }
570
571    #[test]
572    fn test_pseudo_inv() {
573        use crate::linalg::{DMatrix, SMatrix};
574        let mut mat = DMatrix::from_element(1, 3, 0.0);
575        mat[(0, 0)] = -1407.273208782421;
576        mat[(0, 1)] = -2146.3100013104886;
577        mat[(0, 2)] = 84.05022886527551;
578
579        println!("{}", pseudo_inverse!(&mat).unwrap());
580
581        let mut mat = SMatrix::<f64, 1, 3>::zeros();
582        mat[(0, 0)] = -1407.273208782421;
583        mat[(0, 1)] = -2146.3100013104886;
584        mat[(0, 2)] = 84.05022886527551;
585
586        println!("{}", pseudo_inverse!(&mat).unwrap());
587
588        let mut mat = SMatrix::<f64, 3, 1>::zeros();
589        mat[(0, 0)] = -1407.273208782421;
590        mat[(1, 0)] = -2146.3100013104886;
591        mat[(2, 0)] = 84.05022886527551;
592
593        println!("{}", pseudo_inverse!(&mat).unwrap());
594
595        // Compare a pseudo inverse with a true inverse
596        let mat = SMatrix::<f64, 2, 2>::new(3.0, 4.0, -2.0, 1.0);
597        println!("{}", mat.try_inverse().unwrap());
598
599        println!("{}", pseudo_inverse!(&mat).unwrap());
600    }
601
602    #[test]
603    fn spherical() {
604        for v in &[
605            Vector3::<f64>::x(),
606            Vector3::<f64>::y(),
607            Vector3::<f64>::z(),
608            Vector3::<f64>::zeros(),
609            Vector3::<f64>::new(159.1, 561.2, 756.3),
610        ] {
611            let (range_ρ, θ, φ) = cartesian_to_spherical(v);
612            let v_prime = spherical_to_cartesian(range_ρ, θ, φ);
613
614            assert!(rss_errors(v, &v_prime) < 1e-12, "{} != {}", v, &v_prime);
615        }
616    }
617
618    #[rustfmt::skip]
619    #[test]
620    fn test_diagonality() {
621        assert!(!is_diagonal(&Matrix3::new(10.0, 0.0, 0.0,
622                                           1.0, 5.0, 0.0,
623                                           0.0, 0.0, 2.0)),
624            "lower triangular"
625        );
626        assert!(!is_diagonal(&Matrix3::new(10.0, 1.0, 0.0,
627                                           1.0, 5.0, 0.0,
628                                           0.0, 0.0, 2.0)),
629            "symmetric but not diag"
630        );
631        assert!(!is_diagonal(&Matrix3::new(10.0, 1.0, 0.0,
632                                           0.0, 5.0, 0.0,
633                                           0.0, 0.0, 2.0)),
634            "upper triangular"
635        );
636        assert!(is_diagonal(&Matrix3::new(10.0, 0.0, 0.0,
637                                           0.0, 0.0, 0.0,
638                                           0.0, 0.0, 2.0)),
639            "diagonal with zero diagonal element"
640        );
641        assert!(is_diagonal(&Matrix3::new(10.0, 0.0, 0.0,
642                                          0.0, 5.0, 0.0,
643                                          0.0, 0.0, 2.0)),
644            "diagonal"
645        );
646    }
647
648    #[test]
649    fn test_projv() {
650        assert_eq!(
651            projv(&Vector3::new(6.0, 6.0, 6.0), &Vector3::new(2.0, 0.0, 0.0)),
652            Vector3::new(6.0, 0.0, 0.0)
653        );
654        assert_eq!(
655            projv(&Vector3::new(6.0, 6.0, 6.0), &Vector3::new(-3.0, 0.0, 0.0)),
656            Vector3::new(6.0, 0.0, 0.0)
657        );
658        assert_eq!(
659            projv(&Vector3::new(6.0, 6.0, 0.0), &Vector3::new(0.0, 7.0, 0.0)),
660            Vector3::new(0.0, 6.0, 0.0)
661        );
662        assert_eq!(
663            projv(&Vector3::new(6.0, 0.0, 0.0), &Vector3::new(0.0, 0.0, 9.0)),
664            Vector3::new(0.0, 0.0, 0.0)
665        );
666
667        let a = Vector3::new(1.0, 1.0, 0.0);
668        let b = Vector3::new(1.0, 0.0, 0.0);
669        let result = projv(&a, &b);
670        assert_abs_diff_eq!(result, Vector3::new(1.0, 0.0, 0.0), epsilon = 1e-7);
671    }
672
673    #[test]
674    fn test_rss_errors() {
675        use nalgebra::U3;
676        let prop_err = OVector::<f64, U3>::from_iterator([1.0, 2.0, 3.0]);
677        let cur_state = OVector::<f64, U3>::from_iterator([1.0, 2.0, 3.0]);
678        approx::assert_relative_eq!(rss_errors(&prop_err, &cur_state), 0.0);
679
680        let prop_err = OVector::<f64, U3>::from_iterator([1.0, 2.0, 3.0]);
681        let cur_state = OVector::<f64, U3>::from_iterator([4.0, 5.0, 6.0]);
682        approx::assert_relative_eq!(rss_errors(&prop_err, &cur_state), 5.196152422706632);
683    }
684
685    #[test]
686    fn test_normalize() {
687        let x = 5.0;
688        let min_x = 0.0;
689        let max_x = 10.0;
690        let result = normalize(x, min_x, max_x);
691        approx::assert_relative_eq!(result, 0.0);
692    }
693
694    #[test]
695    fn test_denormalize() {
696        let xp = 0.0;
697        let min_x = 0.0;
698        let max_x = 10.0;
699        let result = denormalize(xp, min_x, max_x);
700        approx::assert_relative_eq!(result, 5.0);
701    }
702
703    #[test]
704    fn test_capitalize() {
705        let s = "hello";
706        let result = capitalize(s);
707        assert_eq!(result, "Hello");
708    }
709
710    #[test]
711    fn test_r2() {
712        let angle = 0.0;
713        let rotation_matrix = r2(angle);
714        assert!((rotation_matrix - Matrix3::identity()).abs().max() <= f64::EPSILON);
715
716        let angle = std::f64::consts::PI / 2.0;
717        let rotation_matrix = r2(angle);
718        let expected_matrix = Matrix3::new(0.0, 0.0, -1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0);
719        assert!((rotation_matrix - expected_matrix).abs().max() <= f64::EPSILON);
720
721        let v = Vector3::new(0.0, 1.0, 0.0);
722        let rotated_v = rotation_matrix * v;
723        assert!((rotated_v - v).norm() <= f64::EPSILON);
724    }
725
726    #[test]
727    fn test_r1() {
728        let angle = 0.0;
729        let rotation_matrix = r1(angle);
730        assert!((rotation_matrix - Matrix3::identity()).abs().max() <= f64::EPSILON);
731
732        let angle = std::f64::consts::PI / 2.0;
733        let rotation_matrix = r1(angle);
734        let expected_matrix = Matrix3::new(1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, -1.0, 0.0);
735        assert!((rotation_matrix - expected_matrix).abs().max() <= f64::EPSILON);
736
737        let v = Vector3::new(1.0, 0.0, 0.0);
738        let rotated_v = rotation_matrix * v;
739        assert!((rotated_v - v).norm() <= f64::EPSILON);
740    }
741
742    #[test]
743    fn test_tilde_matrix() {
744        let vec = Vector3::new(1.0, 2.0, 3.0);
745        let rslt = Matrix3::new(0.0, -3.0, 2.0, 3.0, 0.0, -1.0, -2.0, 1.0, 0.0);
746        assert_eq!(tilde_matrix(&vec), rslt);
747
748        let v = Vector3::new(1.0, 2.0, 3.0);
749        let m = tilde_matrix(&v);
750
751        approx::assert_relative_eq!(m[(0, 0)], 0.0);
752        approx::assert_relative_eq!(m[(0, 1)], -v.z);
753        approx::assert_relative_eq!(m[(0, 2)], v.y);
754        approx::assert_relative_eq!(m[(1, 0)], v.z);
755        approx::assert_relative_eq!(m[(1, 1)], 0.0);
756        approx::assert_relative_eq!(m[(1, 2)], -v.x);
757        approx::assert_relative_eq!(m[(2, 0)], -v.y);
758        approx::assert_relative_eq!(m[(2, 1)], v.x);
759        approx::assert_relative_eq!(m[(2, 2)], 0.0);
760    }
761
762    #[test]
763    fn test_perpv() {
764        assert_eq!(
765            perpv(&Vector3::new(6.0, 6.0, 6.0), &Vector3::new(2.0, 0.0, 0.0)),
766            Vector3::new(0.0, 6.0, 6.0)
767        );
768        assert_eq!(
769            perpv(&Vector3::new(6.0, 6.0, 6.0), &Vector3::new(-3.0, 0.0, 0.0)),
770            Vector3::new(0.0, 6.0, 6.0)
771        );
772        assert_eq!(
773            perpv(&Vector3::new(6.0, 6.0, 0.0), &Vector3::new(0.0, 7.0, 0.0)),
774            Vector3::new(6.0, 0.0, 0.0)
775        );
776        assert_eq!(
777            perpv(&Vector3::new(6.0, 0.0, 0.0), &Vector3::new(0.0, 0.0, 9.0)),
778            Vector3::new(6.0, 0.0, 0.0)
779        );
780        let a = Vector3::new(1.0, 1.0, 0.0);
781        let b = Vector3::new(1.0, 0.0, 0.0);
782        let result = perpv(&a, &b);
783        assert_abs_diff_eq!(result, Vector3::new(0.0, 1.0, 0.0), epsilon = 1e-7);
784    }
785
786    #[test]
787    fn test_r3() {
788        let angle = 0.0;
789        let rotation_matrix = r3(angle);
790        assert!((rotation_matrix - Matrix3::identity()).abs().max() <= f64::EPSILON);
791
792        let angle = std::f64::consts::PI / 2.0;
793        let rotation_matrix = r3(angle);
794        let expected_matrix = Matrix3::new(0.0, 1.0, 0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 1.0);
795        assert!((rotation_matrix - expected_matrix).abs().max() <= f64::EPSILON);
796
797        let v = Vector3::new(0.0, 0.0, 1.0);
798        let rotated_v = rotation_matrix * v;
799        assert!((rotated_v - v).norm() <= f64::EPSILON);
800    }
801
802    #[test]
803    fn test_rotv() {
804        use approx::assert_abs_diff_eq;
805        let v = Vector3::new(1.0, 0.0, 0.0);
806        let axis = Vector3::new(0.0, 0.0, 1.0);
807        let theta = std::f64::consts::PI / 2.0;
808        let result = rotv(&v, &axis, theta);
809        assert_abs_diff_eq!(result, Vector3::new(0.0, 1.0, 0.0), epsilon = 1e-7);
810    }
811}