1use core::fmt::Debug;
44
45use dyn_stack::{MemStack, StackReq};
46use faer::matrix_free::{BiLinOp, BiPrecond, LinOp, Precond};
47use faer::{MatMut, MatRef, Par};
48use faer_traits::{ComplexField, Index};
49
50mod apply;
51mod build;
52
53#[derive(Debug, Clone, Copy, PartialEq)]
55pub enum PolyKind {
56 Neumann { omega: f64 },
58 Chebyshev { lambda_min: f64, lambda_max: f64 },
60}
61
62#[derive(Debug, Clone, Copy, PartialEq)]
64pub enum BoundEstimate {
65 Gershgorin,
68 PowerIteration { iters: usize },
71 Manual { lambda_min: f64, lambda_max: f64 },
73}
74
75#[derive(Debug, Clone, Copy, PartialEq)]
77pub struct PolyParams {
78 pub degree: usize,
80 pub kind: PolyKind,
82}
83
84#[derive(Debug, Clone, PartialEq)]
86pub enum PolyError {
87 NonSquareMatrix { nrows: usize, ncols: usize },
89 ZeroDegree,
91 InvalidOmega,
93 InvalidBounds,
95 PatternMismatch,
97}
98
99impl core::fmt::Display for PolyError {
100 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
101 match self {
102 Self::NonSquareMatrix { nrows, ncols } => {
103 write!(f, "matrix must be square but is {nrows}x{ncols}")
104 }
105 Self::ZeroDegree => f.write_str("polynomial degree must be at least 1"),
106 Self::InvalidOmega => f.write_str("neumann omega must be finite and positive"),
107 Self::InvalidBounds => {
108 f.write_str("chebyshev bounds must satisfy 0 < lambda_min < lambda_max")
109 }
110 Self::PatternMismatch => f.write_str("refactorisation pattern does not match"),
111 }
112 }
113}
114
115impl core::error::Error for PolyError {}
116
117#[derive(Debug, Clone)]
119pub(crate) enum Coeffs<T> {
120 Neumann { omega: T },
121 Chebyshev { lambda_min: T, lambda_max: T },
122}
123
124#[derive(Debug, Clone)]
130pub struct Poly<I, T> {
131 pub(crate) dim: usize,
132 pub(crate) degree: usize,
133 pub(crate) a_col_ptr: Vec<I>,
134 pub(crate) a_row_idx: Vec<I>,
135 pub(crate) a_values: Vec<T>,
136 pub(crate) coeffs: Coeffs<T>,
137 pub(crate) recompute: Option<BoundEstimate>,
138}
139
140impl<I, T> Poly<I, T> {
141 #[inline]
143 pub fn dim(&self) -> usize {
144 self.dim
145 }
146
147 #[inline]
149 pub fn degree(&self) -> usize {
150 self.degree
151 }
152}
153
154impl<I, T> LinOp<T> for Poly<I, T>
155where
156 I: Index,
157 T: ComplexField + Debug + Sync,
158{
159 fn apply_scratch(&self, rhs_ncols: usize, _par: Par) -> StackReq {
160 apply::run_scratch(self, rhs_ncols)
161 }
162
163 fn nrows(&self) -> usize {
164 self.dim
165 }
166
167 fn ncols(&self) -> usize {
168 self.dim
169 }
170
171 fn apply(&self, out: MatMut<'_, T>, rhs: MatRef<'_, T>, par: Par, stack: &mut MemStack) {
172 apply::apply_out(self, out, rhs, par, stack);
173 }
174
175 fn conj_apply(&self, out: MatMut<'_, T>, rhs: MatRef<'_, T>, par: Par, stack: &mut MemStack) {
176 apply::apply_out(self, out, rhs, par, stack);
180 }
181}
182
183impl<I, T> Precond<T> for Poly<I, T>
184where
185 I: Index,
186 T: ComplexField + Debug + Sync,
187{
188 fn apply_in_place_scratch(&self, rhs_ncols: usize, _par: Par) -> StackReq {
189 apply::inplace_scratch(self, rhs_ncols)
190 }
191
192 fn apply_in_place(&self, rhs: MatMut<'_, T>, par: Par, stack: &mut MemStack) {
193 apply::apply_inplace(self, rhs, par, stack);
194 }
195
196 fn conj_apply_in_place(&self, rhs: MatMut<'_, T>, par: Par, stack: &mut MemStack) {
197 apply::apply_inplace(self, rhs, par, stack);
198 }
199}
200
201impl<I, T> BiLinOp<T> for Poly<I, T>
202where
203 I: Index,
204 T: ComplexField + Debug + Sync,
205{
206 fn transpose_apply_scratch(&self, rhs_ncols: usize, _par: Par) -> StackReq {
207 apply::run_scratch(self, rhs_ncols)
208 }
209
210 fn transpose_apply(
211 &self,
212 out: MatMut<'_, T>,
213 rhs: MatRef<'_, T>,
214 par: Par,
215 stack: &mut MemStack,
216 ) {
217 apply::apply_out(self, out, rhs, par, stack);
220 }
221
222 fn adjoint_apply(
223 &self,
224 out: MatMut<'_, T>,
225 rhs: MatRef<'_, T>,
226 par: Par,
227 stack: &mut MemStack,
228 ) {
229 apply::apply_out(self, out, rhs, par, stack);
230 }
231}
232
233impl<I, T> BiPrecond<T> for Poly<I, T>
234where
235 I: Index,
236 T: ComplexField + Debug + Sync,
237{
238 fn transpose_apply_in_place_scratch(&self, rhs_ncols: usize, _par: Par) -> StackReq {
239 apply::inplace_scratch(self, rhs_ncols)
240 }
241
242 fn transpose_apply_in_place(&self, rhs: MatMut<'_, T>, par: Par, stack: &mut MemStack) {
243 apply::apply_inplace(self, rhs, par, stack);
244 }
245
246 fn adjoint_apply_in_place(&self, rhs: MatMut<'_, T>, par: Par, stack: &mut MemStack) {
247 apply::apply_inplace(self, rhs, par, stack);
248 }
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254 use core::mem::MaybeUninit;
255 use faer::sparse::{SparseColMat, Triplet};
256 use faer::{Mat, MatRef};
257
258 fn with_stack(req: StackReq, f: impl FnOnce(&mut MemStack)) {
259 let nbytes = req.unaligned_bytes_required().max(1);
260 let mut buf = vec![MaybeUninit::<u8>::uninit(); nbytes].into_boxed_slice();
261 f(MemStack::new(&mut buf));
262 }
263
264 fn assert_close(lhs: MatRef<'_, f64>, rhs: MatRef<'_, f64>, tol: f64) {
265 assert_eq!(lhs.nrows(), rhs.nrows());
266 assert_eq!(lhs.ncols(), rhs.ncols());
267 for j in 0..lhs.ncols() {
268 for i in 0..lhs.nrows() {
269 let diff = (*lhs.get(i, j) - *rhs.get(i, j)).abs();
270 assert!(
271 diff <= tol,
272 "mismatch at ({i}, {j}): lhs={}, rhs={}, diff={diff}",
273 *lhs.get(i, j),
274 *rhs.get(i, j),
275 );
276 }
277 }
278 }
279
280 fn to_dense(a: &SparseColMat<usize, f64>) -> Mat<f64> {
281 let n = a.nrows();
282 let mut out = Mat::<f64>::zeros(n, a.ncols());
283 let a_ref = a.as_ref();
284 for j in 0..a.ncols() {
285 let rows = a_ref.symbolic().row_idx_of_col_raw(j);
286 let vals = a_ref.val_of_col(j);
287 for (r, v) in rows.iter().zip(vals.iter()) {
288 *out.as_mut().get_mut(*r, j) = *v;
289 }
290 }
291 out
292 }
293
294 fn tridiagonal(n: usize, diag: f64, off: f64) -> SparseColMat<usize, f64> {
295 let mut triplets = Vec::new();
296 for i in 0..n {
297 triplets.push(Triplet::new(i, i, diag));
298 if i > 0 {
299 triplets.push(Triplet::new(i, i - 1, off));
300 triplets.push(Triplet::new(i - 1, i, off));
301 }
302 }
303 SparseColMat::try_new_from_triplets(n, n, &triplets).unwrap()
304 }
305
306 fn laplacian_2d(grid: usize) -> SparseColMat<usize, f64> {
307 let n = grid * grid;
308 let mut triplets = Vec::new();
309 for gy in 0..grid {
310 for gx in 0..grid {
311 let idx = gy * grid + gx;
312 triplets.push(Triplet::new(idx, idx, 4.0));
313 if gx > 0 {
314 triplets.push(Triplet::new(idx, idx - 1, -1.0));
315 }
316 if gx + 1 < grid {
317 triplets.push(Triplet::new(idx, idx + 1, -1.0));
318 }
319 if gy > 0 {
320 triplets.push(Triplet::new(idx, idx - grid, -1.0));
321 }
322 if gy + 1 < grid {
323 triplets.push(Triplet::new(idx, idx + grid, -1.0));
324 }
325 }
326 }
327 SparseColMat::try_new_from_triplets(n, n, &triplets).unwrap()
328 }
329
330 fn apply_inplace(pc: &Poly<usize, f64>, rhs: &mut Mat<f64>) {
331 with_stack(pc.apply_in_place_scratch(rhs.ncols(), Par::Seq), |stack| {
332 pc.apply_in_place(rhs.as_mut(), Par::Seq, stack);
333 });
334 }
335
336 fn residual_ratio(a: &SparseColMat<usize, f64>, pc: &Poly<usize, f64>, b: &Mat<f64>) -> f64 {
337 let a_dense = to_dense(a);
338 let mut x = b.clone();
339 apply_inplace(pc, &mut x);
340 let residual = &a_dense * &x - b;
341 let b_norm: f64 = b.as_ref().col(0).iter().map(|v| v * v).sum::<f64>().sqrt();
342 let r_norm: f64 = residual
343 .as_ref()
344 .col(0)
345 .iter()
346 .map(|v| v * v)
347 .sum::<f64>()
348 .sqrt();
349 r_norm / b_norm
350 }
351
352 #[test]
353 fn neumann_higher_degree_reduces_residual() {
354 let a = tridiagonal(20, 2.5, -1.0);
355 let b = Mat::<f64>::from_fn(20, 1, |i, _| (i % 5) as f64 - 2.0);
356 let mk = |deg| {
358 Poly::try_new(
359 a.as_ref(),
360 PolyParams {
361 degree: deg,
362 kind: PolyKind::Neumann { omega: 0.4 },
363 },
364 )
365 .unwrap()
366 };
367 let r_low = residual_ratio(&a, &mk(2), &b);
368 let r_high = residual_ratio(&a, &mk(12), &b);
369 assert!(
370 r_high < r_low,
371 "higher Neumann degree should reduce residual: {r_high} !< {r_low}"
372 );
373 }
374
375 #[test]
376 fn chebyshev_reduces_residual_with_exact_bounds() {
377 let grid = 8;
380 let a = laplacian_2d(grid);
381 let n = a.nrows();
382 let h = std::f64::consts::PI / (grid as f64 + 1.0);
383 let lam_min = 4.0 - 4.0 * (h).cos();
384 let lam_max = 4.0 - 4.0 * (grid as f64 * h).cos();
385 let pc = Poly::try_new(
386 a.as_ref(),
387 PolyParams {
388 degree: 8,
389 kind: PolyKind::Chebyshev {
390 lambda_min: lam_min,
391 lambda_max: lam_max,
392 },
393 },
394 )
395 .unwrap();
396 let b = Mat::<f64>::from_fn(n, 1, |i, _| (i % 7) as f64 - 3.0);
397 let ratio = residual_ratio(&a, &pc, &b);
398 assert!(ratio < 0.5, "Chebyshev residual ratio {ratio} too large");
399 }
400
401 #[test]
402 fn out_of_place_matches_in_place() {
403 let a = tridiagonal(12, 3.0, -1.0);
404 let pc = Poly::try_new(
405 a.as_ref(),
406 PolyParams {
407 degree: 5,
408 kind: PolyKind::Chebyshev {
409 lambda_min: 1.0,
410 lambda_max: 5.0,
411 },
412 },
413 )
414 .unwrap();
415 let rhs = Mat::<f64>::from_fn(12, 2, |i, j| ((i + 3 * j) % 7) as f64 - 3.0);
416
417 let mut out = Mat::<f64>::zeros(12, 2);
418 with_stack(pc.apply_scratch(2, Par::Seq), |stack| {
419 pc.apply(out.as_mut(), rhs.as_ref(), Par::Seq, stack);
420 });
421 let mut inplace = rhs.clone();
422 apply_inplace(&pc, &mut inplace);
423 assert_close(out.as_ref(), inplace.as_ref(), 1e-12);
424 }
425
426 #[test]
427 fn auto_power_iteration_builds_and_helps() {
428 let a = laplacian_2d(8);
429 let n = a.nrows();
430 let pc = Poly::try_new_auto(a.as_ref(), 6, BoundEstimate::PowerIteration { iters: 30 })
431 .unwrap();
432 let b = Mat::<f64>::from_fn(n, 1, |i, _| (i % 7) as f64 - 3.0);
433 let ratio = residual_ratio(&a, &pc, &b);
434 assert!(ratio < 1.0, "auto Chebyshev should not diverge: {ratio}");
435 }
436
437 #[test]
438 fn refactorize_updates_values() {
439 let a1 = tridiagonal(10, 3.0, -1.0);
440 let a2 = tridiagonal(10, 4.0, -1.5);
441 let params = PolyParams {
442 degree: 4,
443 kind: PolyKind::Neumann { omega: 0.3 },
444 };
445 let fresh = Poly::try_new(a2.as_ref(), params).unwrap();
446 let mut reused = Poly::try_new(a1.as_ref(), params).unwrap();
447 reused.refactorize(a2.as_ref()).unwrap();
448 assert_eq!(fresh.a_values, reused.a_values);
449 }
450
451 #[test]
452 fn rejects_bad_params() {
453 let a = tridiagonal(4, 3.0, -1.0);
454 assert_eq!(
455 Poly::try_new(
456 a.as_ref(),
457 PolyParams {
458 degree: 0,
459 kind: PolyKind::Neumann { omega: 0.3 }
460 }
461 )
462 .unwrap_err(),
463 PolyError::ZeroDegree
464 );
465 assert_eq!(
466 Poly::try_new(
467 a.as_ref(),
468 PolyParams {
469 degree: 3,
470 kind: PolyKind::Neumann { omega: -1.0 }
471 }
472 )
473 .unwrap_err(),
474 PolyError::InvalidOmega
475 );
476 assert_eq!(
477 Poly::try_new(
478 a.as_ref(),
479 PolyParams {
480 degree: 3,
481 kind: PolyKind::Chebyshev {
482 lambda_min: 2.0,
483 lambda_max: 1.0
484 }
485 }
486 )
487 .unwrap_err(),
488 PolyError::InvalidBounds
489 );
490 }
491}