1use core::array;
13use core::fmt;
14use core::ops::{Add, Mul, Sub};
15
16use crate::math;
17
18#[derive(Clone, Copy, PartialEq)]
20pub struct Matrix<const R: usize, const C: usize> {
21 rows: [[f64; C]; R],
22}
23
24pub type Vector<const N: usize> = Matrix<N, 1>;
26
27impl<const R: usize, const C: usize> Matrix<R, C> {
28 pub const ZERO: Self = Self {
30 rows: [[0.0; C]; R],
31 };
32
33 #[must_use]
35 pub const fn from_rows(rows: [[f64; C]; R]) -> Self {
36 Self { rows }
37 }
38
39 #[must_use]
41 pub fn from_fn(mut element: impl FnMut(usize, usize) -> f64) -> Self {
42 Self {
43 rows: array::from_fn(|row| array::from_fn(|column| element(row, column))),
44 }
45 }
46
47 #[must_use]
49 pub const fn rows(&self) -> &[[f64; C]; R] {
50 &self.rows
51 }
52
53 #[must_use]
55 pub fn get(&self, row: usize, column: usize) -> Option<f64> {
56 self.rows.get(row)?.get(column).copied()
57 }
58
59 pub fn set(&mut self, row: usize, column: usize, value: f64) -> bool {
62 match self.rows.get_mut(row).and_then(|r| r.get_mut(column)) {
63 Some(slot) => {
64 *slot = value;
65 true
66 }
67 None => false,
68 }
69 }
70
71 fn at(&self, row: usize, column: usize) -> f64 {
74 self.get(row, column).unwrap_or(0.0)
75 }
76
77 #[must_use]
79 pub fn transpose(&self) -> Matrix<C, R> {
80 Matrix::from_fn(|row, column| self.at(column, row))
81 }
82
83 #[must_use]
85 pub fn scaled(&self, factor: f64) -> Self {
86 Self::from_fn(|row, column| self.at(row, column) * factor)
87 }
88
89 #[must_use]
91 pub fn is_finite(&self) -> bool {
92 self.rows.iter().flatten().all(|value| value.is_finite())
93 }
94
95 #[must_use]
97 pub fn max_abs(&self) -> f64 {
98 self.rows
99 .iter()
100 .flatten()
101 .fold(0.0, |largest, &value| largest.max(math::abs(value)))
102 }
103}
104
105impl<const N: usize> Matrix<N, N> {
106 #[must_use]
108 pub fn identity() -> Self {
109 Self::from_fn(|row, column| if row == column { 1.0 } else { 0.0 })
110 }
111
112 #[must_use]
114 pub fn diagonal(values: [f64; N]) -> Self {
115 Self::from_fn(|row, column| {
116 if row == column {
117 values.get(row).copied().unwrap_or(0.0)
118 } else {
119 0.0
120 }
121 })
122 }
123
124 #[must_use]
126 pub fn trace(&self) -> f64 {
127 (0..N).map(|index| self.at(index, index)).sum()
128 }
129
130 #[must_use]
135 pub fn symmetrised(&self) -> Self {
136 Self::from_fn(|row, column| f64::midpoint(self.at(row, column), self.at(column, row)))
137 }
138
139 #[must_use]
146 pub fn cholesky(&self) -> Option<Cholesky<N>> {
147 let tolerance = self.max_abs() * f64::EPSILON * math::count_to_f64(N);
148 let mut lower = Self::ZERO;
149 for j in 0..N {
150 let mut diagonal = self.at(j, j);
151 for k in 0..j {
152 diagonal -= lower.at(j, k) * lower.at(j, k);
153 }
154 if !diagonal.is_finite() || diagonal < -tolerance {
155 return None;
156 }
157 let pivot = if diagonal <= tolerance {
160 0.0
161 } else {
162 math::sqrt(diagonal)
163 };
164 lower.set(j, j, pivot);
165 for i in (j + 1)..N {
166 let mut sum = self.at(i, j);
167 for k in 0..j {
168 sum -= lower.at(i, k) * lower.at(j, k);
169 }
170 let value = if pivot > 0.0 { sum / pivot } else { 0.0 };
171 if !value.is_finite() {
172 return None;
173 }
174 lower.set(i, j, value);
175 }
176 }
177 Some(Cholesky { lower })
178 }
179
180 #[must_use]
183 pub fn is_covariance(&self) -> bool {
184 let scale = self.max_abs().max(f64::MIN_POSITIVE);
185 let symmetric = (0..N).all(|row| {
186 (0..N).all(|column| {
187 math::abs(self.at(row, column) - self.at(column, row)) <= scale * 1e-9
188 })
189 });
190 symmetric && self.is_finite() && self.cholesky().is_some()
191 }
192}
193
194#[derive(Debug, Clone, Copy, PartialEq)]
196pub struct Cholesky<const N: usize> {
197 lower: Matrix<N, N>,
198}
199
200impl<const N: usize> Cholesky<N> {
201 #[must_use]
203 pub const fn lower(&self) -> &Matrix<N, N> {
204 &self.lower
205 }
206
207 #[must_use]
212 pub fn solve<const M: usize>(&self, rhs: &Matrix<N, M>) -> Option<Matrix<N, M>> {
213 let mut y = Matrix::<N, M>::ZERO;
215 for column in 0..M {
216 for i in 0..N {
217 let mut sum = rhs.at(i, column);
218 for k in 0..i {
219 sum -= self.lower.at(i, k) * y.at(k, column);
220 }
221 let pivot = self.lower.at(i, i);
222 if pivot == 0.0 {
223 if math::abs(sum) > 0.0 {
224 return None;
225 }
226 y.set(i, column, 0.0);
227 } else {
228 y.set(i, column, sum / pivot);
229 }
230 }
231 }
232 let mut x = Matrix::<N, M>::ZERO;
234 for column in 0..M {
235 for i in (0..N).rev() {
236 let mut sum = y.at(i, column);
237 for k in (i + 1)..N {
238 sum -= self.lower.at(k, i) * x.at(k, column);
239 }
240 let pivot = self.lower.at(i, i);
241 if pivot == 0.0 {
242 if math::abs(sum) > 0.0 {
243 return None;
244 }
245 x.set(i, column, 0.0);
246 } else {
247 x.set(i, column, sum / pivot);
248 }
249 }
250 }
251 x.is_finite().then_some(x)
252 }
253
254 #[must_use]
256 pub fn determinant(&self) -> f64 {
257 let product: f64 = (0..N).map(|index| self.lower.at(index, index)).product();
258 product * product
259 }
260}
261
262impl<const N: usize> Vector<N> {
263 #[must_use]
265 pub fn from_column(values: [f64; N]) -> Self {
266 Self::from_fn(|row, _| values.get(row).copied().unwrap_or(0.0))
267 }
268
269 #[must_use]
271 pub fn to_column(&self) -> [f64; N] {
272 array::from_fn(|row| self.at(row, 0))
273 }
274
275 #[must_use]
277 pub fn element(&self, row: usize) -> Option<f64> {
278 self.get(row, 0)
279 }
280
281 #[must_use]
283 pub fn dot(&self, other: &Self) -> f64 {
284 (0..N).map(|row| self.at(row, 0) * other.at(row, 0)).sum()
285 }
286
287 #[must_use]
289 pub fn norm(&self) -> f64 {
290 math::sqrt(self.dot(self))
291 }
292}
293
294impl<const R: usize, const C: usize> Add for Matrix<R, C> {
295 type Output = Self;
296
297 fn add(self, other: Self) -> Self {
298 Self::from_fn(|row, column| self.at(row, column) + other.at(row, column))
299 }
300}
301
302impl<const R: usize, const C: usize> Sub for Matrix<R, C> {
303 type Output = Self;
304
305 fn sub(self, other: Self) -> Self {
306 Self::from_fn(|row, column| self.at(row, column) - other.at(row, column))
307 }
308}
309
310impl<const R: usize, const K: usize, const C: usize> Mul<Matrix<K, C>> for Matrix<R, K> {
311 type Output = Matrix<R, C>;
312
313 fn mul(self, other: Matrix<K, C>) -> Matrix<R, C> {
314 Matrix::from_fn(|row, column| (0..K).map(|k| self.at(row, k) * other.at(k, column)).sum())
315 }
316}
317
318impl<const R: usize, const C: usize> fmt::Debug for Matrix<R, C> {
319 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
320 f.debug_list().entries(self.rows.iter()).finish()
321 }
322}
323
324#[cfg(test)]
325#[allow(clippy::unwrap_used, clippy::float_cmp)]
326mod tests {
327 use super::*;
328
329 #[test]
330 fn products_and_transposes_come_out_right() {
331 let a = Matrix::from_rows([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]);
332 let b = Matrix::from_rows([[7.0, 8.0], [9.0, 10.0], [11.0, 12.0]]);
333 let product = a * b;
334 assert_eq!(product.rows(), &[[58.0, 64.0], [139.0, 154.0]]);
335 assert_eq!(a.transpose().rows(), &[[1.0, 4.0], [2.0, 5.0], [3.0, 6.0]]);
336 assert_eq!((a + a).rows(), &a.scaled(2.0).rows().clone());
337 assert_eq!((a - a), Matrix::ZERO);
338 assert_eq!(a.get(1, 2), Some(6.0));
339 assert_eq!(a.get(2, 0), None);
340 let mut c = a;
341 assert!(!c.set(5, 5, 1.0));
342 assert!(c.set(0, 0, 9.0));
343 assert_eq!(c.get(0, 0), Some(9.0));
344 assert_eq!(Matrix::<3, 3>::identity().trace(), 3.0);
345 }
346
347 #[test]
348 fn cholesky_factors_and_solves_a_positive_definite_system() {
349 let a = Matrix::from_rows([
350 [4.0, 12.0, -16.0],
351 [12.0, 37.0, -43.0],
352 [-16.0, -43.0, 98.0],
353 ]);
354 let factor = a.cholesky().unwrap();
355 assert_eq!(
356 factor.lower().rows(),
357 &[[2.0, 0.0, 0.0], [6.0, 1.0, 0.0], [-8.0, 5.0, 3.0]]
358 );
359 assert!((factor.determinant() - 36.0).abs() < 1e-9);
360 let b = Vector::from_column([1.0, 2.0, 3.0]);
361 let x = factor.solve(&b).unwrap();
362 let residual = a * x - b;
363 assert!(residual.max_abs() < 1e-12);
364 assert!(a.is_covariance());
365 }
366
367 #[test]
368 fn an_indefinite_matrix_has_no_factor() {
369 let indefinite = Matrix::from_rows([[1.0, 2.0], [2.0, 1.0]]);
370 assert!(indefinite.cholesky().is_none());
371 assert!(!indefinite.is_covariance());
372 let asymmetric = Matrix::from_rows([[1.0, 0.5], [0.0, 1.0]]);
373 assert!(!asymmetric.is_covariance());
374 let nan = Matrix::from_rows([[f64::NAN, 0.0], [0.0, 1.0]]);
375 assert!(nan.cholesky().is_none());
376 }
377
378 #[test]
379 fn a_semidefinite_matrix_factors_with_a_zero_pivot_and_solves_where_it_can() {
380 let semidefinite = Matrix::from_rows([[4.0, 0.0], [0.0, 0.0]]);
382 let factor = semidefinite.cholesky().unwrap();
383 assert_eq!(factor.lower().rows(), &[[2.0, 0.0], [0.0, 0.0]]);
384 assert!(semidefinite.is_covariance());
385 assert!(factor.solve(&Vector::from_column([2.0, 0.0])).is_some());
387 assert!(factor.solve(&Vector::from_column([0.0, 1.0])).is_none());
388 }
389
390 #[test]
391 fn symmetrising_removes_round_off_asymmetry() {
392 let slightly_off = Matrix::from_rows([[1.0, 0.5 + 1e-16], [0.5 - 1e-16, 1.0]]);
393 let fixed = slightly_off.symmetrised();
394 assert_eq!(fixed.get(0, 1), fixed.get(1, 0));
395 }
396
397 #[test]
398 fn vectors_dot_and_measure() {
399 let v = Vector::from_column([3.0, 4.0]);
400 assert_eq!(v.norm(), 5.0);
401 assert_eq!(v.dot(&v), 25.0);
402 assert_eq!(v.to_column(), [3.0, 4.0]);
403 assert_eq!(v.element(1), Some(4.0));
404 assert_eq!(v.element(2), None);
405 assert_eq!(Matrix::<2, 2>::diagonal([1.0, 2.0]).trace(), 3.0);
406 }
407}