Skip to main content

pykep_core/math/
linalg.rs

1// Copyright (c) 2023-2026 Dario Izzo (dario.izzo@gmail.com)
2//                         Advanced Concepts Team, European Space Agency (ESA)
3// Copyright (c) 2026 pykep-rust contributors
4// SPDX-License-Identifier: MPL-2.0
5//
6// Adapted from src/linalg.cpp and include/kep3/linalg.hpp at pykep commit
7// 53b1ca3ce5f8c223f96819b2ea9ba16c3719e63e.
8
9//! Allocation-free operations for small fixed vectors and matrices.
10
11use crate::error::{ensure_finite, ensure_finite_output};
12use crate::{Matrix3, PykepError, Result, Vector3};
13
14fn validate_vector(parameter: &'static str, vector: &Vector3) -> Result<()> {
15    for &value in vector {
16        ensure_finite(parameter, value)?;
17    }
18    Ok(())
19}
20
21/// Computes the Euclidean dot product of two three-vectors.
22///
23/// Input and output units follow ordinary dimensional multiplication.
24///
25/// # Errors
26///
27/// Returns an error for non-finite components or binary64 overflow.
28pub fn dot(left: &Vector3, right: &Vector3) -> Result<f64> {
29    validate_vector("left", left)?;
30    validate_vector("right", right)?;
31    ensure_finite_output(
32        "dot",
33        left[0].mul_add(right[0], left[1].mul_add(right[1], left[2] * right[2])),
34    )
35}
36
37/// Computes the Euclidean norm of a three-vector.
38///
39/// The result has the same units as the vector components.
40///
41/// # Errors
42///
43/// Returns an error for non-finite components or binary64 overflow.
44pub fn norm(vector: &Vector3) -> Result<f64> {
45    validate_vector("vector", vector)?;
46    ensure_finite_output("norm", vector[0].hypot(vector[1]).hypot(vector[2]))
47}
48
49/// Returns a normalized unit vector.
50///
51/// # Errors
52///
53/// Returns an error for non-finite input, a zero vector, or binary64
54/// overflow.
55pub fn normalize(vector: &Vector3) -> Result<Vector3> {
56    let magnitude = norm(vector)?;
57    if magnitude == 0.0 {
58        return Err(PykepError::SingularGeometry {
59            operation: "normalize",
60        });
61    }
62    let result = [
63        vector[0] / magnitude,
64        vector[1] / magnitude,
65        vector[2] / magnitude,
66    ];
67    validate_vector("normalized_vector", &result)?;
68    Ok(result)
69}
70
71/// Computes the right-handed cross product `left × right`.
72///
73/// Input and output units follow ordinary dimensional multiplication.
74///
75/// # Errors
76///
77/// Returns an error for non-finite components or binary64 overflow.
78pub fn cross(left: &Vector3, right: &Vector3) -> Result<Vector3> {
79    validate_vector("left", left)?;
80    validate_vector("right", right)?;
81    let result = [
82        left[1].mul_add(right[2], -(left[2] * right[1])),
83        left[2].mul_add(right[0], -(left[0] * right[2])),
84        left[0].mul_add(right[1], -(left[1] * right[0])),
85    ];
86    for &value in &result {
87        ensure_finite_output("cross", value)?;
88    }
89    Ok(result)
90}
91
92/// Returns the row-major skew-symmetric matrix `[vector]×`.
93///
94/// Multiplying the result by another vector produces their cross product.
95///
96/// # Errors
97///
98/// Returns an error when a component is NaN or infinite.
99pub fn skew(vector: &Vector3) -> Result<Matrix3> {
100    validate_vector("vector", vector)?;
101    Ok([
102        [0.0, -vector[2], vector[1]],
103        [vector[2], 0.0, -vector[0]],
104        [-vector[1], vector[0], 0.0],
105    ])
106}
107
108fn paired_batch<R>(
109    left: &[Vector3],
110    right: &[Vector3],
111    workers: usize,
112    operation: fn(&Vector3, &Vector3) -> Result<R>,
113) -> Result<Vec<R>>
114where
115    R: Send,
116{
117    if left.len() != right.len() {
118        return Err(PykepError::DimensionMismatch {
119            expected: left.len(),
120            actual: right.len(),
121        });
122    }
123    let inputs: Vec<_> = left.iter().zip(right).collect();
124    crate::batch::try_map(&inputs, workers, |(left, right)| operation(left, right))
125}
126
127/// Evaluates ordered three-vector dot products.
128///
129/// # Errors
130///
131/// Returns a dimension mismatch, invalid worker count, or the first vector
132/// validation error in input order.
133pub fn dot_batch(left: &[Vector3], right: &[Vector3], workers: usize) -> Result<Vec<f64>> {
134    paired_batch(left, right, workers, dot)
135}
136
137/// Evaluates ordered three-vector norms.
138///
139/// # Errors
140///
141/// Returns an invalid worker count or the first vector validation error in
142/// input order.
143pub fn norm_batch(vectors: &[Vector3], workers: usize) -> Result<Vec<f64>> {
144    crate::batch::try_map(vectors, workers, norm)
145}
146
147/// Normalizes an ordered batch of three-vectors.
148///
149/// # Errors
150///
151/// Returns an invalid worker count or the first vector validation/geometry
152/// error in input order.
153pub fn normalize_batch(vectors: &[Vector3], workers: usize) -> Result<Vec<Vector3>> {
154    crate::batch::try_map(vectors, workers, normalize)
155}
156
157/// Evaluates ordered right-handed cross products.
158///
159/// # Errors
160///
161/// Returns a dimension mismatch, invalid worker count, or the first vector
162/// validation error in input order.
163pub fn cross_batch(left: &[Vector3], right: &[Vector3], workers: usize) -> Result<Vec<Vector3>> {
164    paired_batch(left, right, workers, cross)
165}
166
167/// Builds an ordered batch of skew-symmetric cross-product matrices.
168///
169/// # Errors
170///
171/// Returns an invalid worker count or the first vector validation error in
172/// input order.
173pub fn skew_batch(vectors: &[Vector3], workers: usize) -> Result<Vec<Matrix3>> {
174    crate::batch::try_map(vectors, workers, skew)
175}
176
177/// Returns an `N × N` row-major identity matrix.
178#[must_use]
179pub fn identity<const N: usize>() -> [[f64; N]; N] {
180    let mut result = [[0.0; N]; N];
181    for (index, row) in result.iter_mut().enumerate() {
182        row[index] = 1.0;
183    }
184    result
185}
186
187/// Multiplies fixed row-major `M × K` and `K × N` matrices.
188///
189/// # Errors
190///
191/// Returns an error for any non-finite input or output component.
192pub fn matrix_multiply<const M: usize, const K: usize, const N: usize>(
193    left: &[[f64; K]; M],
194    right: &[[f64; N]; K],
195) -> Result<[[f64; N]; M]> {
196    let mut result = [[0.0; N]; M];
197    for row in 0..M {
198        for column in 0..N {
199            let mut value = 0.0;
200            for inner in 0..K {
201                ensure_finite("left", left[row][inner])?;
202                ensure_finite("right", right[inner][column])?;
203                value = left[row][inner].mul_add(right[inner][column], value);
204            }
205            result[row][column] = ensure_finite_output("matrix_multiply", value)?;
206        }
207    }
208    Ok(result)
209}
210
211/// Multiplies a fixed row-major `M × N` matrix by an `N`-vector.
212///
213/// # Errors
214///
215/// Returns an error for any non-finite input or output component.
216pub fn matrix_vector_multiply<const M: usize, const N: usize>(
217    matrix: &[[f64; N]; M],
218    vector: &[f64; N],
219) -> Result<[f64; M]> {
220    let mut result = [0.0; M];
221    for row in 0..M {
222        let mut value = 0.0;
223        for column in 0..N {
224            ensure_finite("matrix", matrix[row][column])?;
225            ensure_finite("vector", vector[column])?;
226            value = matrix[row][column].mul_add(vector[column], value);
227        }
228        result[row] = ensure_finite_output("matrix_vector_multiply", value)?;
229    }
230    Ok(result)
231}
232
233/// Transposes a fixed row-major `M × N` matrix.
234#[must_use]
235pub fn transpose<const M: usize, const N: usize>(matrix: &[[f64; N]; M]) -> [[f64; M]; N] {
236    let mut result = [[0.0; M]; N];
237    for (row, values) in matrix.iter().enumerate() {
238        for (column, &value) in values.iter().enumerate() {
239            result[column][row] = value;
240        }
241    }
242    result
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248
249    #[test]
250    fn vector_operations_match_right_handed_geometry() {
251        let x = [1.0, 0.0, 0.0];
252        let y = [0.0, 1.0, 0.0];
253        assert_eq!(dot(&x, &y), Ok(0.0));
254        assert_eq!(norm(&[3.0, 4.0, 12.0]), Ok(13.0));
255        assert_eq!(cross(&x, &y), Ok([0.0, 0.0, 1.0]));
256        assert_eq!(
257            matrix_vector_multiply(&skew(&x).unwrap(), &y),
258            cross(&x, &y)
259        );
260    }
261
262    #[test]
263    fn normalization_is_stable_for_large_finite_vectors() {
264        let value = normalize(&[1e308, 1e308, 0.0]).unwrap();
265        let expected = core::f64::consts::FRAC_1_SQRT_2;
266        assert!((value[0] - expected).abs() < 2e-16);
267        assert!((value[1] - expected).abs() < 2e-16);
268    }
269
270    #[test]
271    fn zero_and_non_finite_vectors_are_rejected() {
272        assert!(matches!(
273            normalize(&[0.0; 3]),
274            Err(PykepError::SingularGeometry { .. })
275        ));
276        assert!(dot(&[f64::NAN, 0.0, 0.0], &[1.0; 3]).is_err());
277    }
278
279    #[test]
280    fn fixed_matrix_operations_preserve_layout() {
281        let left = [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
282        let right = [[7.0, 8.0], [9.0, 10.0], [11.0, 12.0]];
283        assert_eq!(
284            matrix_multiply(&left, &right),
285            Ok([[58.0, 64.0], [139.0, 154.0]])
286        );
287        assert_eq!(transpose(&left), [[1.0, 4.0], [2.0, 5.0], [3.0, 6.0]]);
288        assert_eq!(
289            identity::<3>(),
290            [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]
291        );
292    }
293
294    #[test]
295    fn ordered_vector_batches_match_scalar_operations() {
296        let left = [[1.0, 2.0, 3.0], [-2.0, 0.5, 4.0]];
297        let right = [[4.0, -1.0, 0.5], [1.0, 3.0, -2.0]];
298        assert_eq!(
299            dot_batch(&left, &right, 2).unwrap(),
300            left.iter()
301                .zip(&right)
302                .map(|(left, right)| dot(left, right).unwrap())
303                .collect::<Vec<_>>()
304        );
305        assert_eq!(
306            norm_batch(&left, 2).unwrap(),
307            left.iter()
308                .map(|value| norm(value).unwrap())
309                .collect::<Vec<_>>()
310        );
311        assert_eq!(
312            normalize_batch(&left, 2).unwrap(),
313            left.iter()
314                .map(|value| normalize(value).unwrap())
315                .collect::<Vec<_>>()
316        );
317        assert_eq!(
318            cross_batch(&left, &right, 2).unwrap(),
319            left.iter()
320                .zip(&right)
321                .map(|(left, right)| cross(left, right).unwrap())
322                .collect::<Vec<_>>()
323        );
324        assert_eq!(
325            skew_batch(&left, 2).unwrap(),
326            left.iter()
327                .map(|value| skew(value).unwrap())
328                .collect::<Vec<_>>()
329        );
330        assert!(dot_batch(&left, &right[..1], 2).is_err());
331    }
332}