Skip to main content

ledge_core/
matrix.rs

1//! A small row-major dense matrix used at the API boundary.
2//!
3//! Ledge deliberately owns this type instead of exposing a particular linear
4//! algebra dependency. The solver only forms matrices whose smaller dimension
5//! is the number of factors plus explicit linear constraints.
6
7use std::ops::{Index, IndexMut};
8
9use thiserror::Error;
10
11/// Errors raised while constructing a [`Matrix`].
12#[derive(Debug, Error, Clone, PartialEq, Eq)]
13pub enum MatrixError {
14    /// The requested element count overflows `usize`.
15    #[error("matrix dimensions {rows}x{cols} overflow the address space")]
16    DimensionsOverflow {
17        /// Requested row count.
18        rows: usize,
19        /// Requested column count.
20        cols: usize,
21    },
22    /// The data length does not match `rows * cols`.
23    #[error("matrix shape {rows}x{cols} requires {expected} values, got {actual}")]
24    InvalidShape {
25        /// Requested row count.
26        rows: usize,
27        /// Requested column count.
28        cols: usize,
29        /// Required number of values.
30        expected: usize,
31        /// Supplied number of values.
32        actual: usize,
33    },
34    /// Rows supplied to `from_rows` do not have equal lengths.
35    #[error("matrix row {row} has {actual} columns; expected {expected}")]
36    RaggedRows {
37        /// Zero-based row index.
38        row: usize,
39        /// Expected column count.
40        expected: usize,
41        /// Actual column count.
42        actual: usize,
43    },
44}
45
46/// A contiguous row-major dense matrix.
47#[derive(Clone, Debug, PartialEq)]
48#[cfg_attr(
49    feature = "serde",
50    derive(serde::Serialize, serde::Deserialize),
51    serde(try_from = "MatrixData", into = "MatrixData")
52)]
53pub struct Matrix {
54    rows: usize,
55    cols: usize,
56    data: Vec<f64>,
57}
58
59/// Wire format for [`Matrix`]: deserialization rebuilds through
60/// [`Matrix::new`], so shape and storage can never disagree.
61#[cfg(feature = "serde")]
62#[derive(serde::Serialize, serde::Deserialize)]
63struct MatrixData {
64    rows: usize,
65    cols: usize,
66    data: Vec<f64>,
67}
68
69#[cfg(feature = "serde")]
70impl TryFrom<MatrixData> for Matrix {
71    type Error = MatrixError;
72
73    fn try_from(data: MatrixData) -> Result<Self, Self::Error> {
74        Self::new(data.rows, data.cols, data.data)
75    }
76}
77
78#[cfg(feature = "serde")]
79impl From<Matrix> for MatrixData {
80    fn from(matrix: Matrix) -> Self {
81        Self {
82            rows: matrix.rows,
83            cols: matrix.cols,
84            data: matrix.data,
85        }
86    }
87}
88
89impl Matrix {
90    /// Builds a matrix from row-major data.
91    ///
92    /// # Errors
93    ///
94    /// Returns [`MatrixError::DimensionsOverflow`] if the dimensions overflow,
95    /// or [`MatrixError::InvalidShape`] when the data length is wrong.
96    pub fn new(rows: usize, cols: usize, data: Vec<f64>) -> Result<Self, MatrixError> {
97        let expected = rows
98            .checked_mul(cols)
99            .ok_or(MatrixError::DimensionsOverflow { rows, cols })?;
100        if data.len() != expected {
101            return Err(MatrixError::InvalidShape {
102                rows,
103                cols,
104                expected,
105                actual: data.len(),
106            });
107        }
108        Ok(Self { rows, cols, data })
109    }
110
111    /// Builds a matrix from nested rows.
112    ///
113    /// # Errors
114    ///
115    /// Returns [`MatrixError::RaggedRows`] if row lengths differ.
116    pub fn from_rows(rows: Vec<Vec<f64>>) -> Result<Self, MatrixError> {
117        let row_count = rows.len();
118        let cols = rows.first().map_or(0, Vec::len);
119        let mut data = Vec::with_capacity(row_count.saturating_mul(cols));
120        for (row_index, row) in rows.into_iter().enumerate() {
121            if row.len() != cols {
122                return Err(MatrixError::RaggedRows {
123                    row: row_index,
124                    expected: cols,
125                    actual: row.len(),
126                });
127            }
128            data.extend(row);
129        }
130        Ok(Self {
131            rows: row_count,
132            cols,
133            data,
134        })
135    }
136
137    /// Returns a zero-filled matrix.
138    #[must_use]
139    pub fn zeros(rows: usize, cols: usize) -> Self {
140        Self {
141            rows,
142            cols,
143            data: vec![0.0; rows.saturating_mul(cols)],
144        }
145    }
146
147    /// Number of rows.
148    #[must_use]
149    pub const fn rows(&self) -> usize {
150        self.rows
151    }
152
153    /// Number of columns.
154    #[must_use]
155    pub const fn cols(&self) -> usize {
156        self.cols
157    }
158
159    /// Row-major backing storage.
160    #[must_use]
161    pub fn as_slice(&self) -> &[f64] {
162        &self.data
163    }
164
165    /// Mutable row-major backing storage.
166    #[must_use]
167    pub fn as_mut_slice(&mut self) -> &mut [f64] {
168        &mut self.data
169    }
170
171    /// Returns one row.
172    #[must_use]
173    pub fn row(&self, row: usize) -> &[f64] {
174        let start = row * self.cols;
175        &self.data[start..start + self.cols]
176    }
177
178    pub(crate) fn mul_vec(&self, x: &[f64]) -> Vec<f64> {
179        debug_assert_eq!(self.cols, x.len());
180        (0..self.rows).map(|row| dot(self.row(row), x)).collect()
181    }
182
183    pub(crate) fn transpose_mul_add(&self, x: &[f64], output: &mut [f64]) {
184        debug_assert_eq!(self.rows, x.len());
185        debug_assert_eq!(self.cols, output.len());
186        for (row, &scale) in x.iter().enumerate() {
187            for col in 0..self.cols {
188                output[col] += self[(row, col)] * scale;
189            }
190        }
191    }
192}
193
194impl Index<(usize, usize)> for Matrix {
195    type Output = f64;
196
197    fn index(&self, (row, col): (usize, usize)) -> &Self::Output {
198        &self.data[row * self.cols + col]
199    }
200}
201
202impl IndexMut<(usize, usize)> for Matrix {
203    fn index_mut(&mut self, (row, col): (usize, usize)) -> &mut Self::Output {
204        &mut self.data[row * self.cols + col]
205    }
206}
207
208pub(crate) fn dot(left: &[f64], right: &[f64]) -> f64 {
209    debug_assert_eq!(left.len(), right.len());
210    left.iter().zip(right).map(|(a, b)| a * b).sum()
211}
212
213pub(crate) fn norm_inf(values: &[f64]) -> f64 {
214    values.iter().fold(0.0, |largest, value| {
215        if value.is_nan() {
216            f64::NAN
217        } else {
218            largest.max(value.abs())
219        }
220    })
221}