use core::fmt::Debug;
use dyn_stack::{MemStack, StackReq};
use faer::matrix_free::{BiLinOp, BiPrecond, LinOp, Precond};
use faer::{MatMut, MatRef, Par};
use faer_traits::{ComplexField, Index};
mod apply;
mod build;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PolyKind {
Neumann { omega: f64 },
Chebyshev { lambda_min: f64, lambda_max: f64 },
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum BoundEstimate {
Gershgorin,
PowerIteration { iters: usize },
Manual { lambda_min: f64, lambda_max: f64 },
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PolyParams {
pub degree: usize,
pub kind: PolyKind,
}
#[derive(Debug, Clone, PartialEq)]
pub enum PolyError {
NonSquareMatrix { nrows: usize, ncols: usize },
ZeroDegree,
InvalidOmega,
InvalidBounds,
PatternMismatch,
}
impl core::fmt::Display for PolyError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::NonSquareMatrix { nrows, ncols } => {
write!(f, "matrix must be square but is {nrows}x{ncols}")
}
Self::ZeroDegree => f.write_str("polynomial degree must be at least 1"),
Self::InvalidOmega => f.write_str("neumann omega must be finite and positive"),
Self::InvalidBounds => {
f.write_str("chebyshev bounds must satisfy 0 < lambda_min < lambda_max")
}
Self::PatternMismatch => f.write_str("refactorisation pattern does not match"),
}
}
}
impl core::error::Error for PolyError {}
#[derive(Debug, Clone)]
pub(crate) enum Coeffs<T> {
Neumann { omega: T },
Chebyshev { lambda_min: T, lambda_max: T },
}
#[derive(Debug, Clone)]
pub struct Poly<I, T> {
pub(crate) dim: usize,
pub(crate) degree: usize,
pub(crate) a_col_ptr: Vec<I>,
pub(crate) a_row_idx: Vec<I>,
pub(crate) a_values: Vec<T>,
pub(crate) coeffs: Coeffs<T>,
pub(crate) recompute: Option<BoundEstimate>,
}
impl<I, T> Poly<I, T> {
#[inline]
pub fn dim(&self) -> usize {
self.dim
}
#[inline]
pub fn degree(&self) -> usize {
self.degree
}
}
impl<I, T> LinOp<T> for Poly<I, T>
where
I: Index,
T: ComplexField + Debug + Sync,
{
fn apply_scratch(&self, rhs_ncols: usize, _par: Par) -> StackReq {
apply::run_scratch(self, rhs_ncols)
}
fn nrows(&self) -> usize {
self.dim
}
fn ncols(&self) -> usize {
self.dim
}
fn apply(&self, out: MatMut<'_, T>, rhs: MatRef<'_, T>, par: Par, stack: &mut MemStack) {
apply::apply_out(self, out, rhs, par, stack);
}
fn conj_apply(&self, out: MatMut<'_, T>, rhs: MatRef<'_, T>, par: Par, stack: &mut MemStack) {
apply::apply_out(self, out, rhs, par, stack);
}
}
impl<I, T> Precond<T> for Poly<I, T>
where
I: Index,
T: ComplexField + Debug + Sync,
{
fn apply_in_place_scratch(&self, rhs_ncols: usize, _par: Par) -> StackReq {
apply::inplace_scratch(self, rhs_ncols)
}
fn apply_in_place(&self, rhs: MatMut<'_, T>, par: Par, stack: &mut MemStack) {
apply::apply_inplace(self, rhs, par, stack);
}
fn conj_apply_in_place(&self, rhs: MatMut<'_, T>, par: Par, stack: &mut MemStack) {
apply::apply_inplace(self, rhs, par, stack);
}
}
impl<I, T> BiLinOp<T> for Poly<I, T>
where
I: Index,
T: ComplexField + Debug + Sync,
{
fn transpose_apply_scratch(&self, rhs_ncols: usize, _par: Par) -> StackReq {
apply::run_scratch(self, rhs_ncols)
}
fn transpose_apply(
&self,
out: MatMut<'_, T>,
rhs: MatRef<'_, T>,
par: Par,
stack: &mut MemStack,
) {
apply::apply_out(self, out, rhs, par, stack);
}
fn adjoint_apply(
&self,
out: MatMut<'_, T>,
rhs: MatRef<'_, T>,
par: Par,
stack: &mut MemStack,
) {
apply::apply_out(self, out, rhs, par, stack);
}
}
impl<I, T> BiPrecond<T> for Poly<I, T>
where
I: Index,
T: ComplexField + Debug + Sync,
{
fn transpose_apply_in_place_scratch(&self, rhs_ncols: usize, _par: Par) -> StackReq {
apply::inplace_scratch(self, rhs_ncols)
}
fn transpose_apply_in_place(&self, rhs: MatMut<'_, T>, par: Par, stack: &mut MemStack) {
apply::apply_inplace(self, rhs, par, stack);
}
fn adjoint_apply_in_place(&self, rhs: MatMut<'_, T>, par: Par, stack: &mut MemStack) {
apply::apply_inplace(self, rhs, par, stack);
}
}
#[cfg(test)]
mod tests {
use super::*;
use core::mem::MaybeUninit;
use faer::sparse::{SparseColMat, Triplet};
use faer::{Mat, MatRef};
fn with_stack(req: StackReq, f: impl FnOnce(&mut MemStack)) {
let nbytes = req.unaligned_bytes_required().max(1);
let mut buf = vec![MaybeUninit::<u8>::uninit(); nbytes].into_boxed_slice();
f(MemStack::new(&mut buf));
}
fn assert_close(lhs: MatRef<'_, f64>, rhs: MatRef<'_, f64>, tol: f64) {
assert_eq!(lhs.nrows(), rhs.nrows());
assert_eq!(lhs.ncols(), rhs.ncols());
for j in 0..lhs.ncols() {
for i in 0..lhs.nrows() {
let diff = (*lhs.get(i, j) - *rhs.get(i, j)).abs();
assert!(
diff <= tol,
"mismatch at ({i}, {j}): lhs={}, rhs={}, diff={diff}",
*lhs.get(i, j),
*rhs.get(i, j),
);
}
}
}
fn to_dense(a: &SparseColMat<usize, f64>) -> Mat<f64> {
let n = a.nrows();
let mut out = Mat::<f64>::zeros(n, a.ncols());
let a_ref = a.as_ref();
for j in 0..a.ncols() {
let rows = a_ref.symbolic().row_idx_of_col_raw(j);
let vals = a_ref.val_of_col(j);
for (r, v) in rows.iter().zip(vals.iter()) {
*out.as_mut().get_mut(*r, j) = *v;
}
}
out
}
fn tridiagonal(n: usize, diag: f64, off: f64) -> SparseColMat<usize, f64> {
let mut triplets = Vec::new();
for i in 0..n {
triplets.push(Triplet::new(i, i, diag));
if i > 0 {
triplets.push(Triplet::new(i, i - 1, off));
triplets.push(Triplet::new(i - 1, i, off));
}
}
SparseColMat::try_new_from_triplets(n, n, &triplets).unwrap()
}
fn laplacian_2d(grid: usize) -> SparseColMat<usize, f64> {
let n = grid * grid;
let mut triplets = Vec::new();
for gy in 0..grid {
for gx in 0..grid {
let idx = gy * grid + gx;
triplets.push(Triplet::new(idx, idx, 4.0));
if gx > 0 {
triplets.push(Triplet::new(idx, idx - 1, -1.0));
}
if gx + 1 < grid {
triplets.push(Triplet::new(idx, idx + 1, -1.0));
}
if gy > 0 {
triplets.push(Triplet::new(idx, idx - grid, -1.0));
}
if gy + 1 < grid {
triplets.push(Triplet::new(idx, idx + grid, -1.0));
}
}
}
SparseColMat::try_new_from_triplets(n, n, &triplets).unwrap()
}
fn apply_inplace(pc: &Poly<usize, f64>, rhs: &mut Mat<f64>) {
with_stack(pc.apply_in_place_scratch(rhs.ncols(), Par::Seq), |stack| {
pc.apply_in_place(rhs.as_mut(), Par::Seq, stack);
});
}
fn residual_ratio(a: &SparseColMat<usize, f64>, pc: &Poly<usize, f64>, b: &Mat<f64>) -> f64 {
let a_dense = to_dense(a);
let mut x = b.clone();
apply_inplace(pc, &mut x);
let residual = &a_dense * &x - b;
let b_norm: f64 = b.as_ref().col(0).iter().map(|v| v * v).sum::<f64>().sqrt();
let r_norm: f64 = residual
.as_ref()
.col(0)
.iter()
.map(|v| v * v)
.sum::<f64>()
.sqrt();
r_norm / b_norm
}
#[test]
fn neumann_higher_degree_reduces_residual() {
let a = tridiagonal(20, 2.5, -1.0);
let b = Mat::<f64>::from_fn(20, 1, |i, _| (i % 5) as f64 - 2.0);
let mk = |deg| {
Poly::try_new(
a.as_ref(),
PolyParams {
degree: deg,
kind: PolyKind::Neumann { omega: 0.4 },
},
)
.unwrap()
};
let r_low = residual_ratio(&a, &mk(2), &b);
let r_high = residual_ratio(&a, &mk(12), &b);
assert!(
r_high < r_low,
"higher Neumann degree should reduce residual: {r_high} !< {r_low}"
);
}
#[test]
fn chebyshev_reduces_residual_with_exact_bounds() {
let grid = 8;
let a = laplacian_2d(grid);
let n = a.nrows();
let h = std::f64::consts::PI / (grid as f64 + 1.0);
let lam_min = 4.0 - 4.0 * (h).cos();
let lam_max = 4.0 - 4.0 * (grid as f64 * h).cos();
let pc = Poly::try_new(
a.as_ref(),
PolyParams {
degree: 8,
kind: PolyKind::Chebyshev {
lambda_min: lam_min,
lambda_max: lam_max,
},
},
)
.unwrap();
let b = Mat::<f64>::from_fn(n, 1, |i, _| (i % 7) as f64 - 3.0);
let ratio = residual_ratio(&a, &pc, &b);
assert!(ratio < 0.5, "Chebyshev residual ratio {ratio} too large");
}
#[test]
fn out_of_place_matches_in_place() {
let a = tridiagonal(12, 3.0, -1.0);
let pc = Poly::try_new(
a.as_ref(),
PolyParams {
degree: 5,
kind: PolyKind::Chebyshev {
lambda_min: 1.0,
lambda_max: 5.0,
},
},
)
.unwrap();
let rhs = Mat::<f64>::from_fn(12, 2, |i, j| ((i + 3 * j) % 7) as f64 - 3.0);
let mut out = Mat::<f64>::zeros(12, 2);
with_stack(pc.apply_scratch(2, Par::Seq), |stack| {
pc.apply(out.as_mut(), rhs.as_ref(), Par::Seq, stack);
});
let mut inplace = rhs.clone();
apply_inplace(&pc, &mut inplace);
assert_close(out.as_ref(), inplace.as_ref(), 1e-12);
}
#[test]
fn auto_power_iteration_builds_and_helps() {
let a = laplacian_2d(8);
let n = a.nrows();
let pc = Poly::try_new_auto(a.as_ref(), 6, BoundEstimate::PowerIteration { iters: 30 })
.unwrap();
let b = Mat::<f64>::from_fn(n, 1, |i, _| (i % 7) as f64 - 3.0);
let ratio = residual_ratio(&a, &pc, &b);
assert!(ratio < 1.0, "auto Chebyshev should not diverge: {ratio}");
}
#[test]
fn refactorize_updates_values() {
let a1 = tridiagonal(10, 3.0, -1.0);
let a2 = tridiagonal(10, 4.0, -1.5);
let params = PolyParams {
degree: 4,
kind: PolyKind::Neumann { omega: 0.3 },
};
let fresh = Poly::try_new(a2.as_ref(), params).unwrap();
let mut reused = Poly::try_new(a1.as_ref(), params).unwrap();
reused.refactorize(a2.as_ref()).unwrap();
assert_eq!(fresh.a_values, reused.a_values);
}
#[test]
fn rejects_bad_params() {
let a = tridiagonal(4, 3.0, -1.0);
assert_eq!(
Poly::try_new(
a.as_ref(),
PolyParams {
degree: 0,
kind: PolyKind::Neumann { omega: 0.3 }
}
)
.unwrap_err(),
PolyError::ZeroDegree
);
assert_eq!(
Poly::try_new(
a.as_ref(),
PolyParams {
degree: 3,
kind: PolyKind::Neumann { omega: -1.0 }
}
)
.unwrap_err(),
PolyError::InvalidOmega
);
assert_eq!(
Poly::try_new(
a.as_ref(),
PolyParams {
degree: 3,
kind: PolyKind::Chebyshev {
lambda_min: 2.0,
lambda_max: 1.0
}
}
)
.unwrap_err(),
PolyError::InvalidBounds
);
}
}