1#[derive(Debug, Clone)]
18pub struct SparseMatrix {
19 rows: usize,
20 cols: usize,
21 row_starts: Vec<usize>,
22 col_indices: Vec<usize>,
23 values: Vec<f64>,
24}
25
26impl SparseMatrix {
27 #[must_use]
33 pub fn from_triplets(rows: usize, cols: usize, triplets: &[(usize, usize, f64)]) -> Self {
34 let mut sorted: Vec<(usize, usize, f64)> = triplets
35 .iter()
36 .inspect(|(r, c, _)| {
37 assert!(*r < rows && *c < cols, "triplet outside the matrix");
38 })
39 .copied()
40 .collect();
41 sorted.sort_by_key(|&(r, c, _)| (r, c));
42
43 let mut row_starts = vec![0usize; rows + 1];
44 let mut col_indices = Vec::with_capacity(sorted.len());
45 let mut values: Vec<f64> = Vec::with_capacity(sorted.len());
46 let mut next = sorted.into_iter().peekable();
47 for (r, start) in row_starts.iter_mut().enumerate().take(rows) {
48 let row_begin = col_indices.len();
49 *start = row_begin;
50 while let Some(&(tr, c, v)) = next.peek() {
51 if tr != r {
52 break;
53 }
54 next.next();
55 if col_indices.len() > row_begin && col_indices.last() == Some(&c) {
56 let last = values.len() - 1;
57 values[last] += v;
58 } else {
59 col_indices.push(c);
60 values.push(v);
61 }
62 }
63 }
64 row_starts[rows] = col_indices.len();
65 Self {
66 rows,
67 cols,
68 row_starts,
69 col_indices,
70 values,
71 }
72 }
73
74 #[must_use]
76 pub fn shape(&self) -> (usize, usize) {
77 (self.rows, self.cols)
78 }
79
80 #[must_use]
82 pub fn stored(&self) -> usize {
83 self.values.len()
84 }
85
86 #[must_use]
91 pub fn multiply(&self, x: &[f64]) -> Vec<f64> {
92 assert_eq!(x.len(), self.cols, "vector length must match columns");
93 let mut y = vec![0.0; self.rows];
94 for (y_r, window) in y.iter_mut().zip(self.row_starts.windows(2)) {
95 let mut sum = 0.0;
96 for i in window[0]..window[1] {
97 sum = self.values[i].mul_add(x[self.col_indices[i]], sum);
98 }
99 *y_r = sum;
100 }
101 y
102 }
103
104 #[must_use]
109 pub fn transpose_multiply(&self, x: &[f64]) -> Vec<f64> {
110 assert_eq!(x.len(), self.rows, "vector length must match rows");
111 let mut y = vec![0.0; self.cols];
112 for (window, xr) in self.row_starts.windows(2).zip(x) {
113 for i in window[0]..window[1] {
114 y[self.col_indices[i]] = self.values[i].mul_add(*xr, y[self.col_indices[i]]);
115 }
116 }
117 y
118 }
119}
120
121fn dot(a: &[f64], b: &[f64]) -> f64 {
122 a.iter().zip(b).fold(0.0, |acc, (x, y)| x.mul_add(*y, acc))
123}
124
125#[must_use]
134pub fn least_squares_cgnr(
135 a: &SparseMatrix,
136 b: &[f64],
137 tolerance: f64,
138 max_iterations: usize,
139) -> Option<Vec<f64>> {
140 let (rows, cols) = a.shape();
141 assert_eq!(b.len(), rows, "right-hand side must match rows");
142 let mut x = vec![0.0; cols];
143 let mut r = b.to_vec();
144 let mut z = a.transpose_multiply(&r);
145 let target = tolerance * dot(&z, &z).sqrt().max(f64::MIN_POSITIVE);
146 let mut p = z.clone();
147 let mut zz = dot(&z, &z);
148 if zz.sqrt() <= target {
149 return Some(x);
150 }
151 for _ in 0..max_iterations {
152 let w = a.multiply(&p);
153 let ww = dot(&w, &w);
154 if ww <= 0.0 {
155 return Some(x);
158 }
159 let alpha = zz / ww;
160 for (xi, pi) in x.iter_mut().zip(&p) {
161 *xi = alpha.mul_add(*pi, *xi);
162 }
163 for (ri, wi) in r.iter_mut().zip(&w) {
164 *ri = alpha.mul_add(-wi, *ri);
165 }
166 z = a.transpose_multiply(&r);
167 let zz_next = dot(&z, &z);
168 if zz_next.sqrt() <= target {
169 return Some(x);
170 }
171 let beta = zz_next / zz;
172 zz = zz_next;
173 for (pi, zi) in p.iter_mut().zip(&z) {
174 *pi = beta.mul_add(*pi, *zi);
175 }
176 }
177 None
178}
179
180#[cfg(test)]
181#[allow(clippy::unwrap_used)]
182mod tests {
183 use super::*;
184
185 #[test]
186 fn triplets_assemble_sum_and_multiply() {
187 let a = SparseMatrix::from_triplets(
190 2,
191 3,
192 &[(0, 0, 1.0), (0, 2, 1.0), (1, 1, 3.0), (0, 0, 1.0)],
193 );
194 assert_eq!(a.stored(), 3);
195 assert_eq!(a.multiply(&[1.0, 2.0, 3.0]), vec![5.0, 6.0]);
196 assert_eq!(a.transpose_multiply(&[1.0, 1.0]), vec![2.0, 3.0, 1.0]);
197 }
198
199 #[test]
200 fn an_overdetermined_system_lands_on_the_normal_equation_answer() {
201 let a = SparseMatrix::from_triplets(
204 3,
205 2,
206 &[
207 (0, 0, 1.0),
208 (0, 1, 0.0),
209 (1, 0, 1.0),
210 (1, 1, 1.0),
211 (2, 0, 1.0),
212 (2, 1, 2.0),
213 ],
214 );
215 let x = least_squares_cgnr(&a, &[1.0, 2.0, 4.0], 1e-14, 100).unwrap();
216 assert!((x[0] - 5.0 / 6.0).abs() < 1e-10, "{x:?}");
217 assert!((x[1] - 1.5).abs() < 1e-10, "{x:?}");
218 }
219
220 #[test]
221 fn a_rank_deficient_system_returns_the_minimum_norm_solution() {
222 let a = SparseMatrix::from_triplets(1, 2, &[(0, 0, 1.0), (0, 1, 1.0)]);
225 let x = least_squares_cgnr(&a, &[2.0], 1e-14, 50).unwrap();
226 assert!(
227 (x[0] - 1.0).abs() < 1e-12 && (x[1] - 1.0).abs() < 1e-12,
228 "{x:?}"
229 );
230 }
231
232 #[test]
233 fn a_zero_matrix_answers_zero_rather_than_spinning() {
234 let a = SparseMatrix::from_triplets(2, 2, &[]);
235 let x = least_squares_cgnr(&a, &[1.0, 1.0], 1e-12, 10).unwrap();
236 assert_eq!(x, vec![0.0, 0.0]);
237 }
238}