Skip to main content

geop_core_math/
matrix.rs

1//! A dense, fixed-size, row-major matrix — [`Vector`]'s 2-D counterpart.
2//!
3//! Exists so callers building a normal-equations system (`JᵀJ`, `Jᵀf`, the
4//! Krawczyk operator's `Y·J(X)`, …) work with actual matrix/vector values
5//! and named operations (`transpose`, `mul_vec`, `mul_mat`) instead of raw
6//! `[[S; N]; M]` arrays and hand-rolled index loops at every call site —
7//! see [`crate::interval_newton`] and [`solve_linear_system`] for the two
8//! main consumers.
9
10use std::ops::{Index, IndexMut};
11
12use crate::{geop_error::GeopResult, scalars::Scalar, vector::Vector};
13
14/// Dense `R×C` matrix, row-major (`self[(row, col)]`).
15#[derive(Debug, Clone, Copy)]
16pub struct Matrix<S, const R: usize, const C: usize> {
17    data: [[S; C]; R],
18}
19
20impl<S: Scalar, const R: usize, const C: usize> Matrix<S, R, C> {
21    pub fn from_rows(data: [[S; C]; R]) -> Self {
22        Self { data }
23    }
24
25    pub fn zero() -> Self {
26        Self {
27            data: [[S::ZERO; C]; R],
28        }
29    }
30
31    /// Build from `C` column vectors, each an `R`-vector.
32    pub fn from_columns(cols: [Vector<S, R>; C]) -> Self {
33        let mut out = Self::zero();
34        for (c, col) in cols.iter().enumerate() {
35            for r in 0..R {
36                out.data[r][c] = col[r];
37            }
38        }
39        out
40    }
41
42    pub fn row(&self, r: usize) -> Vector<S, C> {
43        Vector::from_array(self.data[r])
44    }
45
46    pub fn col(&self, c: usize) -> Vector<S, R> {
47        let mut out = Vector::<S, R>::zero();
48        for r in 0..R {
49            out[r] = self.data[r][c];
50        }
51        out
52    }
53
54    pub fn transpose(&self) -> Matrix<S, C, R> {
55        let mut out = Matrix::<S, C, R>::zero();
56        for r in 0..R {
57            for c in 0..C {
58                out[(c, r)] = self[(r, c)];
59            }
60        }
61        out
62    }
63
64    pub fn add(&self, other: &Self) -> Self {
65        let mut out = Self::zero();
66        for r in 0..R {
67            for c in 0..C {
68                out[(r, c)] = self[(r, c)].add(other[(r, c)]);
69            }
70        }
71        out
72    }
73
74    pub fn sub(&self, other: &Self) -> Self {
75        let mut out = Self::zero();
76        for r in 0..R {
77            for c in 0..C {
78                out[(r, c)] = self[(r, c)].sub(other[(r, c)]);
79            }
80        }
81        out
82    }
83
84    /// `self · v`.
85    pub fn mul_vec(&self, v: &Vector<S, C>) -> Vector<S, R> {
86        let mut out = Vector::<S, R>::zero();
87        for r in 0..R {
88            out[r] = self.row(r).prod_dot(v);
89        }
90        out
91    }
92
93    /// `self · other`.
94    pub fn mul_mat<const K: usize>(&self, other: &Matrix<S, C, K>) -> Matrix<S, R, K> {
95        let mut out = Matrix::<S, R, K>::zero();
96        for r in 0..R {
97            for k in 0..K {
98                out[(r, k)] = self.row(r).prod_dot(&other.col(k));
99            }
100        }
101        out
102    }
103
104    fn swap_rows(&mut self, i: usize, j: usize) {
105        self.data.swap(i, j);
106    }
107}
108
109impl<S: Scalar, const N: usize> Matrix<S, N, N> {
110    pub fn identity() -> Self {
111        let mut out = Self::zero();
112        for i in 0..N {
113            out[(i, i)] = S::ONE;
114        }
115        out
116    }
117}
118
119impl<S, const R: usize, const C: usize> Index<(usize, usize)> for Matrix<S, R, C> {
120    type Output = S;
121    fn index(&self, (r, c): (usize, usize)) -> &S {
122        &self.data[r][c]
123    }
124}
125impl<S, const R: usize, const C: usize> IndexMut<(usize, usize)> for Matrix<S, R, C> {
126    fn index_mut(&mut self, (r, c): (usize, usize)) -> &mut S {
127        &mut self.data[r][c]
128    }
129}
130
131/// Solve the dense `N x N` system `a * x = b` by Gaussian elimination with
132/// partial pivoting, returning `x`.
133///
134/// Errors if the system is singular — which, with interval scalars, means
135/// the pivot *could* be zero, not merely that it is: a pivot straddling
136/// zero carries no usable information about the solution, and dividing by
137/// it would manufacture an arbitrarily wide answer rather than report that
138/// there isn't one. Callers that can proceed without a solution (a
139/// rank-deficient configuration they have a fallback for) should handle the
140/// error rather than pre-screen the matrix.
141///
142/// Pivot selection compares the *sharpened* magnitudes of the candidates.
143/// That is a conditioning choice, not a correctness claim: any row with a
144/// nonzero pivot yields the same solution set, and picking the largest one
145/// only keeps the elimination numerically well-behaved — so resolving the
146/// comparison to a single value (rather than leaving it three-valued) costs
147/// nothing, exactly as in `NurbSurface::project`'s per-iteration sharpening.
148pub fn solve_linear_system<S: Scalar, const N: usize>(
149    a: &Matrix<S, N, N>,
150    b: &Vector<S, N>,
151) -> GeopResult<Vector<S, N>> {
152    let mut a = *a;
153    let mut b = *b;
154
155    for col in 0..N {
156        let mut pivot = col;
157        for row in (col + 1)..N {
158            if a[(row, col)]
159                .abs()
160                .sharpen()
161                .definitely_greater(a[(pivot, col)].abs().sharpen())
162            {
163                pivot = row;
164            }
165        }
166        a.swap_rows(col, pivot);
167        let tmp = b[col];
168        b[col] = b[pivot];
169        b[pivot] = tmp;
170
171        for row in (col + 1)..N {
172            let factor = a[(row, col)].div(a[(col, col)]).map_err(|e| {
173                e.with_context(format!(
174                    "solve_linear_system: singular at column {col}, pivot={:?}",
175                    a[(col, col)]
176                ))
177            })?;
178            for k in col..N {
179                a[(row, k)] = a[(row, k)].sub(factor.mul(a[(col, k)]));
180            }
181            b[row] = b[row].sub(factor.mul(b[col]));
182        }
183    }
184
185    let mut x = Vector::<S, N>::zero();
186    for row in (0..N).rev() {
187        let mut sum = b[row];
188        for k in (row + 1)..N {
189            sum = sum.sub(a[(row, k)].mul(x[k]));
190        }
191        x[row] = sum.div(a[(row, row)]).map_err(|e| {
192            e.with_context(format!(
193                "solve_linear_system: singular at row {row}, pivot={:?}",
194                a[(row, row)]
195            ))
196        })?;
197    }
198    Ok(x)
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204    use crate::for_all_scalars;
205
206    fn check_solves_identity<S: Scalar>() {
207        let a = Matrix::<S, 2, 2>::identity();
208        let b = Vector::<S, 2>::from_array([S::from_f64(3.0), S::from_f64(4.0)]);
209        let x = solve_linear_system(&a, &b).unwrap();
210        assert!(x[0].could_be_equal(S::from_f64(3.0)));
211        assert!(x[1].could_be_equal(S::from_f64(4.0)));
212    }
213    #[test]
214    fn solves_identity() {
215        for_all_scalars!(check_solves_identity);
216    }
217
218    fn check_solves_general_2x2<S: Scalar>() {
219        // [2 1; 1 3] x = [5; 10] -> x = [1, 3]
220        let a = Matrix::from_rows([
221            [S::from_f64(2.0), S::from_f64(1.0)],
222            [S::from_f64(1.0), S::from_f64(3.0)],
223        ]);
224        let b = Vector::from_array([S::from_f64(5.0), S::from_f64(10.0)]);
225        let x = solve_linear_system(&a, &b).unwrap();
226        assert!(x[0].could_be_equal(S::ONE));
227        assert!(x[1].could_be_equal(S::from_f64(3.0)));
228    }
229    #[test]
230    fn solves_general_2x2() {
231        for_all_scalars!(check_solves_general_2x2);
232    }
233
234    fn check_singular_errs<S: Scalar>() {
235        let a = Matrix::from_rows([[S::ONE, S::ONE], [S::ONE, S::ONE]]);
236        let b = Vector::from_array([S::ONE, S::TWO]);
237        assert!(solve_linear_system(&a, &b).is_err());
238    }
239    #[test]
240    fn singular_errs() {
241        for_all_scalars!(check_singular_errs);
242    }
243
244    fn check_transpose_and_mul<S: Scalar>() {
245        let m = Matrix::<S, 2, 3>::from_rows([
246            [S::from_f64(1.0), S::from_f64(2.0), S::from_f64(3.0)],
247            [S::from_f64(4.0), S::from_f64(5.0), S::from_f64(6.0)],
248        ]);
249        let mt = m.transpose();
250        assert!(mt[(0, 1)].could_be_equal(S::from_f64(4.0)));
251        assert!(mt[(2, 0)].could_be_equal(S::from_f64(3.0)));
252
253        let v = Vector::<S, 3>::from_array([S::ONE, S::ONE, S::ONE]);
254        let mv = m.mul_vec(&v);
255        assert!(mv[0].could_be_equal(S::from_f64(6.0)));
256        assert!(mv[1].could_be_equal(S::from_f64(15.0)));
257
258        let mtm = mt.mul_mat(&m); // 3x3
259        assert!(mtm[(0, 0)].could_be_equal(S::from_f64(1.0 + 16.0)));
260    }
261    #[test]
262    fn transpose_and_mul() {
263        for_all_scalars!(check_transpose_and_mul);
264    }
265}