1use crate::error::{all_finite, tolerance, NumericError, NumericResult, Status};
21
22#[derive(Debug, Clone, PartialEq)]
28pub struct SparseMatrix {
29 rows: usize,
30 cols: usize,
31 row_start: Vec<usize>,
33 columns: Vec<usize>,
34 values: Vec<f64>,
35}
36
37impl SparseMatrix {
38 pub fn from_triplets(
48 rows: usize,
49 cols: usize,
50 triplets: &[(usize, usize, f64)],
51 ) -> NumericResult<Self> {
52 let mut order: Vec<usize> = (0..triplets.len()).collect();
53 for &(i, j, v) in triplets {
54 if i >= rows || j >= cols {
55 return Err(NumericError::InvalidArgument {
56 name: "triplet",
57 reason: "must lie inside the matrix shape",
58 });
59 }
60 if !v.is_finite() {
61 return Err(NumericError::NonFiniteInput {
62 name: "triplet value",
63 });
64 }
65 }
66 order.sort_by_key(|&k| (triplets[k].0, triplets[k].1));
69 let mut row_start = vec![0usize; rows + 1];
70 let mut columns: Vec<usize> = Vec::with_capacity(triplets.len());
71 let mut values: Vec<f64> = Vec::with_capacity(triplets.len());
72 let mut last: Option<(usize, usize)> = None;
73 for k in order {
74 let (i, j, v) = triplets[k];
75 if last == Some((i, j)) {
76 *values.last_mut().expect("an entry precedes its duplicate") += v;
77 } else {
78 columns.push(j);
79 values.push(v);
80 row_start[i + 1] += 1;
81 last = Some((i, j));
82 }
83 }
84 for i in 0..rows {
85 row_start[i + 1] += row_start[i];
86 }
87 Ok(Self {
88 rows,
89 cols,
90 row_start,
91 columns,
92 values,
93 })
94 }
95
96 #[must_use]
98 pub fn rows(&self) -> usize {
99 self.rows
100 }
101
102 #[must_use]
104 pub fn cols(&self) -> usize {
105 self.cols
106 }
107
108 #[must_use]
110 pub fn nnz(&self) -> usize {
111 self.values.len()
112 }
113
114 #[must_use]
120 pub fn get(&self, i: usize, j: usize) -> f64 {
121 assert!(i < self.rows && j < self.cols, "matrix index out of range");
122 let (cols, vals) = self.row(i);
123 cols.binary_search(&j).map_or(0.0, |k| vals[k])
124 }
125
126 #[must_use]
132 pub fn row(&self, i: usize) -> (&[usize], &[f64]) {
133 let range = self.row_start[i]..self.row_start[i + 1];
134 (&self.columns[range.clone()], &self.values[range])
135 }
136
137 pub fn mul_vec(&self, x: &[f64]) -> NumericResult<Vec<f64>> {
143 if x.len() != self.cols {
144 return Err(NumericError::DimensionMismatch {
145 name: "vector",
146 expected: self.cols,
147 found: x.len(),
148 });
149 }
150 let mut y = vec![0.0; self.rows];
151 self.mul_into(x, &mut y);
152 Ok(y)
153 }
154
155 fn mul_into(&self, x: &[f64], y: &mut [f64]) {
156 for (i, out) in y.iter_mut().enumerate() {
157 let (cols, vals) = self.row(i);
158 *out = cols.iter().zip(vals).map(|(&j, &v)| v * x[j]).sum();
159 }
160 }
161
162 fn require_symmetric(&self) -> NumericResult<()> {
165 if self.rows != self.cols {
166 return Err(NumericError::DimensionMismatch {
167 name: "symmetric matrix",
168 expected: self.rows,
169 found: self.cols,
170 });
171 }
172 for i in 0..self.rows {
173 let (cols, vals) = self.row(i);
174 for (&j, &v) in cols.iter().zip(vals) {
175 if j <= i {
176 continue;
177 }
178 let w = self.get(j, i);
179 if (v - w).abs() > 16.0 * f64::EPSILON * v.abs().max(w.abs()) {
180 return Err(NumericError::NotSymmetric { row: i, column: j });
181 }
182 }
183 }
184 Ok(())
185 }
186}
187
188#[derive(Debug, Clone, Copy, PartialEq)]
190pub struct ConjugateGradientOptions {
191 pub relative_tolerance: f64,
193 pub max_iterations: Option<usize>,
195}
196
197impl Default for ConjugateGradientOptions {
198 fn default() -> Self {
199 Self {
200 relative_tolerance: 1e-12,
201 max_iterations: None,
202 }
203 }
204}
205
206#[derive(Debug, Clone, PartialEq)]
208#[must_use]
209pub struct ConjugateGradientSolution {
210 pub x: Vec<f64>,
212 pub relative_residual: f64,
215 pub iterations: usize,
217 pub status: Status,
220}
221
222impl ConjugateGradientSolution {
223 pub fn converged(self) -> NumericResult<Self> {
229 if self.status == Status::Converged {
230 Ok(self)
231 } else {
232 Err(NumericError::NotConverged {
233 name: "conjugate gradient",
234 error_estimate: self.relative_residual,
235 })
236 }
237 }
238}
239
240fn dot(a: &[f64], b: &[f64]) -> f64 {
241 a.iter().zip(b).map(|(x, y)| x * y).sum()
242}
243
244fn norm2(a: &[f64]) -> f64 {
245 dot(a, a).sqrt()
246}
247
248pub fn conjugate_gradient(
270 a: &SparseMatrix,
271 b: &[f64],
272 initial: Option<&[f64]>,
273 options: ConjugateGradientOptions,
274) -> NumericResult<ConjugateGradientSolution> {
275 a.require_symmetric()?;
276 let n = a.rows;
277 if b.len() != n {
278 return Err(NumericError::DimensionMismatch {
279 name: "right-hand side",
280 expected: n,
281 found: b.len(),
282 });
283 }
284 all_finite(b, "right-hand side")?;
285 let relative_tolerance = tolerance(options.relative_tolerance, "relative tolerance")?;
286 let mut x = match initial {
287 Some(x0) => {
288 if x0.len() != n {
289 return Err(NumericError::DimensionMismatch {
290 name: "initial guess",
291 expected: n,
292 found: x0.len(),
293 });
294 }
295 all_finite(x0, "initial guess")?;
296 x0.to_vec()
297 }
298 None => vec![0.0; n],
299 };
300 let mut inverse_diagonal = Vec::with_capacity(n);
301 for i in 0..n {
302 let d = a.get(i, i);
303 if d <= 0.0 {
304 return Err(NumericError::NotPositiveDefinite { pivot: i });
305 }
306 inverse_diagonal.push(1.0 / d);
307 }
308 let b_norm = norm2(b);
309 if b_norm == 0.0 {
310 return Ok(ConjugateGradientSolution {
311 x: vec![0.0; n],
312 relative_residual: 0.0,
313 iterations: 0,
314 status: Status::Converged,
315 });
316 }
317 let target = relative_tolerance * b_norm;
318 let budget = options.max_iterations.unwrap_or(10 * n + 100);
319
320 let mut ax = vec![0.0; n];
321 let true_residual = |x: &[f64], ax: &mut Vec<f64>, r: &mut Vec<f64>| {
322 a.mul_into(x, ax);
323 for i in 0..n {
324 r[i] = b[i] - ax[i];
325 }
326 norm2(r)
327 };
328 let mut r = vec![0.0; n];
329 let mut r_norm = true_residual(&x, &mut ax, &mut r);
330 let mut z: Vec<f64> = r
331 .iter()
332 .zip(&inverse_diagonal)
333 .map(|(r, d)| r * d)
334 .collect();
335 let mut p = z.clone();
336 let mut rz = dot(&r, &z);
337 let mut ap = vec![0.0; n];
338 let mut iterations = 0;
339 loop {
340 if r_norm <= target {
341 r_norm = true_residual(&x, &mut ax, &mut r);
344 if r_norm <= target {
345 return Ok(ConjugateGradientSolution {
346 x,
347 relative_residual: r_norm / b_norm,
348 iterations,
349 status: Status::Converged,
350 });
351 }
352 for i in 0..n {
353 z[i] = r[i] * inverse_diagonal[i];
354 }
355 p.clone_from(&z);
356 rz = dot(&r, &z);
357 }
358 if iterations >= budget {
359 r_norm = true_residual(&x, &mut ax, &mut r);
360 let status = if r_norm <= target {
361 Status::Converged
362 } else {
363 Status::BudgetExhausted
364 };
365 return Ok(ConjugateGradientSolution {
366 x,
367 relative_residual: r_norm / b_norm,
368 iterations,
369 status,
370 });
371 }
372 a.mul_into(&p, &mut ap);
373 let curvature = dot(&p, &ap);
374 if curvature <= 0.0 {
375 return Err(NumericError::NotPositiveDefinite { pivot: iterations });
376 }
377 let alpha = rz / curvature;
378 for i in 0..n {
379 x[i] += alpha * p[i];
380 r[i] -= alpha * ap[i];
381 }
382 r_norm = norm2(&r);
383 for i in 0..n {
384 z[i] = r[i] * inverse_diagonal[i];
385 }
386 let rz_next = dot(&r, &z);
387 let beta = rz_next / rz;
388 rz = rz_next;
389 for i in 0..n {
390 p[i] = z[i] + beta * p[i];
391 }
392 iterations += 1;
393 }
394}