use sprs::{CsMat, TriMat};
pub struct SparseSystem {
tri: TriMat<f64>,
dim: usize,
}
impl SparseSystem {
pub fn new(dim: usize) -> Self {
Self {
tri: TriMat::new((dim, dim)),
dim,
}
}
pub fn add(&mut self, i: usize, j: usize, val: f64) {
if i < self.dim && j < self.dim {
self.tri.add_triplet(i, j, val);
if i != j {
self.tri.add_triplet(j, i, val);
}
}
}
pub fn add_diag(&mut self, i: usize, val: f64) {
if i < self.dim {
self.tri.add_triplet(i, i, val);
}
}
pub fn finish(self) -> CsMat<f64> {
self.tri.to_csr()
}
pub fn dim(&self) -> usize {
self.dim
}
}
pub fn build_vertex_index(
mesh: &crate::storage::MeshStorage,
) -> std::collections::HashMap<crate::ids::VertexId, usize> {
mesh.vertex_ids().enumerate().map(|(i, v)| (v, i)).collect()
}
pub fn build_cotan_laplacian(
mesh: &crate::storage::MeshStorage,
v_idx: &std::collections::HashMap<crate::ids::VertexId, usize>,
) -> SparseSystem {
use crate::geometry::cotan_edge_weight;
use crate::traversal::VertexRing;
let n = v_idx.len();
let mut sys = SparseSystem::new(n);
for (&v, &i) in v_idx {
let mut diag = 0.0;
for he in VertexRing::new(mesh, v) {
let neighbor = match mesh.get_halfedge(he) {
Some(h) => h.vertex,
None => continue,
};
if let Some(&j) = v_idx.get(&neighbor) {
let w = cotan_edge_weight(mesh, he).unwrap_or(0.0) / 2.0;
sys.add(i, j, -w);
diag += w;
}
}
sys.add_diag(i, diag);
}
sys
}
pub fn conjugate_gradient(
a: &CsMat<f64>,
b: &[f64],
max_iter: usize,
tol: f64,
) -> Option<Vec<f64>> {
let n = a.rows();
if n != b.len() {
return None;
}
let preconditioner = vec![1.0; n];
conjugate_gradient_preconditioned(a, b, &preconditioner, max_iter, tol)
}
pub fn conjugate_gradient_preconditioned(
a: &CsMat<f64>,
b: &[f64],
preconditioner: &[f64],
max_iter: usize,
tol: f64,
) -> Option<Vec<f64>> {
let n = a.rows();
if n != b.len() || n != preconditioner.len() {
return None;
}
if n == 0 {
return Some(Vec::new());
}
let b_norm = norm2(b);
if b_norm < 1e-30 {
return Some(vec![0.0; n]);
}
let mut x = vec![0.0; n];
let mut r = b.to_vec();
let mut z: Vec<f64> = r
.iter()
.zip(preconditioner.iter())
.map(|(ri, mi)| ri * mi)
.collect();
let mut p = z.clone();
let mut rz_old = dot(&r, &z);
for _iter in 0..max_iter {
let ap = sparse_matvec(a, &p);
let p_ap = dot(&p, &ap);
if p_ap.abs() < 1e-30 {
return None;
}
let alpha = rz_old / p_ap;
for i in 0..n {
x[i] += alpha * p[i];
}
for i in 0..n {
r[i] -= alpha * ap[i];
}
let residual_rel = norm2(&r) / b_norm;
if residual_rel < tol {
return Some(x);
}
for i in 0..n {
z[i] = r[i] * preconditioner[i];
}
let rz_new = dot(&r, &z);
if rz_old.abs() < 1e-30 {
return None;
}
let beta = rz_new / rz_old;
for i in 0..n {
p[i] = z[i] + beta * p[i];
}
rz_old = rz_new;
}
None
}
pub fn jacobi_preconditioner(a: &CsMat<f64>) -> Vec<f64> {
let n = a.rows();
let mut diag_inv = vec![1.0; n];
for (i, slot) in diag_inv.iter_mut().enumerate() {
if let Some(&val) = a.get(i, i)
&& val.abs() > 1e-30
{
*slot = 1.0 / val;
}
}
diag_inv
}
pub fn regularize_diagonal(a: &mut CsMat<f64>, lambda: f64) {
let n = a.rows();
for i in 0..n {
if let Some(val) = a.get_mut(i, i) {
*val += lambda;
}
}
}
fn sparse_matvec(a: &CsMat<f64>, x: &[f64]) -> Vec<f64> {
let mut y = vec![0.0; a.rows()];
for (row_idx, row) in a.outer_iterator().enumerate() {
let mut sum = 0.0;
for (col_idx, &val) in row.iter() {
if col_idx < x.len() {
sum += val * x[col_idx];
}
}
y[row_idx] = sum;
}
y
}
pub(crate) fn dot(a: &[f64], b: &[f64]) -> f64 {
a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
}
pub(crate) fn norm2(v: &[f64]) -> f64 {
dot(v, v).sqrt()
}
pub(crate) mod vec3 {
pub(crate) type Vec3 = [f64; 3];
#[inline]
pub(crate) fn sub(a: Vec3, b: Vec3) -> Vec3 {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
#[inline]
pub(crate) fn add(a: Vec3, b: Vec3) -> Vec3 {
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
#[inline]
pub(crate) fn scale(a: Vec3, s: f64) -> Vec3 {
[a[0] * s, a[1] * s, a[2] * s]
}
#[inline]
pub(crate) fn dot(a: Vec3, b: Vec3) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
#[inline]
pub(crate) fn cross(a: Vec3, b: Vec3) -> Vec3 {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
}
#[inline]
pub(crate) fn length(a: Vec3) -> f64 {
dot(a, a).sqrt()
}
#[inline]
pub(crate) fn normalize(a: Vec3) -> Vec3 {
let l = length(a);
if l < 1e-12 { a } else { scale(a, 1.0 / l) }
}
#[inline]
pub(crate) fn angle_between(u: Vec3, v: Vec3) -> f64 {
let lu = length(u);
let lv = length(v);
if lu < 1e-12 || lv < 1e-12 {
return 0.0;
}
let c = dot(u, v) / (lu * lv);
c.clamp(-1.0, 1.0).acos()
}
#[inline]
pub(crate) fn triangle_area(a: Vec3, b: Vec3, c: Vec3) -> f64 {
0.5 * length(cross(sub(b, a), sub(c, a)))
}
#[inline]
pub(crate) fn triangle_normal(a: Vec3, b: Vec3, c: Vec3) -> Vec3 {
normalize(cross(sub(b, a), sub(c, a)))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cg_identity() {
let mut tri = TriMat::new((3, 3));
tri.add_triplet(0, 0, 1.0);
tri.add_triplet(1, 1, 1.0);
tri.add_triplet(2, 2, 1.0);
let a = tri.to_csr();
let b = vec![1.0, 2.0, 3.0];
let x = conjugate_gradient(&a, &b, 50, 1e-12).unwrap();
for i in 0..3 {
assert!((x[i] - b[i]).abs() < 1e-10);
}
}
#[test]
fn test_cg_small_spd() {
let mut tri = TriMat::new((3, 3));
tri.add_triplet(0, 0, 2.0);
tri.add_triplet(1, 1, 3.0);
tri.add_triplet(2, 2, 2.0);
tri.add_triplet(0, 1, 1.0);
tri.add_triplet(1, 0, 1.0);
tri.add_triplet(1, 2, -1.0);
tri.add_triplet(2, 1, -1.0);
let a = tri.to_csr();
let b = vec![1.0, 0.0, 1.0];
let x = conjugate_gradient(&a, &b, 50, 1e-12).unwrap();
let ax = sparse_matvec(&a, &x);
for i in 0..3 {
assert!(
(ax[i] - b[i]).abs() < 1e-10,
"component {i}: Ax={} b={}",
ax[i],
b[i]
);
}
}
#[test]
fn test_regularize_diagonal() {
let mut tri = TriMat::new((2, 2));
tri.add_triplet(0, 0, 1.0);
tri.add_triplet(1, 1, 2.0);
let mut a = tri.to_csr();
regularize_diagonal(&mut a, 0.5);
assert!((a.get(0, 0).unwrap() - 1.5).abs() < 1e-14);
assert!((a.get(1, 1).unwrap() - 2.5).abs() < 1e-14);
}
#[test]
fn test_sparse_system_builder() {
let mut sys = SparseSystem::new(3);
sys.add(0, 0, 4.0);
sys.add(0, 1, -1.0);
sys.add(1, 2, -2.0);
sys.add_diag(2, 3.0);
let a = sys.finish();
assert_eq!(a.rows(), 3);
assert!((a.get(0, 0).unwrap() - 4.0).abs() < 1e-14);
assert!((a.get(0, 1).unwrap() + 1.0).abs() < 1e-14);
assert!((a.get(1, 0).unwrap() + 1.0).abs() < 1e-14);
assert!((a.get(1, 2).unwrap() + 2.0).abs() < 1e-14);
assert!((a.get(2, 1).unwrap() + 2.0).abs() < 1e-14);
assert!((a.get(2, 2).unwrap() - 3.0).abs() < 1e-14);
}
#[test]
fn cg_dimension_mismatch_returns_none() {
let mut tri = TriMat::new((2, 2));
tri.add_triplet(0, 0, 1.0);
tri.add_triplet(1, 1, 1.0);
let a = tri.to_csr();
let b = vec![1.0, 2.0, 3.0]; assert!(conjugate_gradient(&a, &b, 10, 1e-10).is_none());
}
#[test]
fn cg_empty_matrix_returns_empty() {
let a = TriMat::new((0, 0)).to_csr();
let b: Vec<f64> = vec![];
assert_eq!(conjugate_gradient(&a, &b, 10, 1e-10), Some(Vec::new()));
}
#[test]
fn cg_zero_right_hand_side_returns_zero_solution() {
let mut tri = TriMat::new((3, 3));
tri.add_triplet(0, 0, 1.0);
tri.add_triplet(1, 1, 1.0);
tri.add_triplet(2, 2, 1.0);
let a = tri.to_csr();
let b = vec![0.0, 0.0, 0.0];
assert_eq!(
conjugate_gradient(&a, &b, 10, 1e-10),
Some(vec![0.0, 0.0, 0.0])
);
}
#[test]
fn cg_max_iter_zero_returns_none() {
let mut tri = TriMat::new((3, 3));
tri.add_triplet(0, 0, 4.0);
tri.add_triplet(1, 1, 3.0);
tri.add_triplet(2, 2, 2.0);
tri.add_triplet(0, 1, 1.0);
tri.add_triplet(1, 0, 1.0);
let a = tri.to_csr();
let b = vec![1.0, 2.0, 3.0];
assert!(conjugate_gradient(&a, &b, 0, 1e-10).is_none());
}
#[test]
fn cg_semidefinite_returns_none() {
let mut tri = TriMat::new((2, 2));
tri.add_triplet(0, 0, 1.0);
tri.add_triplet(1, 1, 1.0);
tri.add_triplet(0, 1, -1.0);
tri.add_triplet(1, 0, -1.0);
let a = tri.to_csr();
let b = vec![1.0, 1.0];
assert!(conjugate_gradient(&a, &b, 100, 1e-10).is_none());
}
#[test]
fn cg_known_solution_2x2() {
let mut tri = TriMat::new((2, 2));
tri.add_triplet(0, 0, 4.0);
tri.add_triplet(1, 1, 3.0);
tri.add_triplet(0, 1, 1.0);
tri.add_triplet(1, 0, 1.0);
let a = tri.to_csr();
let b = vec![1.0, 2.0];
let x = conjugate_gradient(&a, &b, 100, 1e-12).expect("SPD 系统应收敛");
let exact = [1.0 / 11.0, 7.0 / 11.0];
for i in 0..2 {
assert!(
(x[i] - exact[i]).abs() < 1e-8,
"分量 {i}: 得到 {},期望 {}",
x[i],
exact[i]
);
}
}
#[test]
fn sparse_system_add_out_of_bounds_silently_drops() {
let mut sys = SparseSystem::new(2);
sys.add(5, 0, 1.0); sys.add_diag(5, 1.0); let a = sys.finish();
assert_eq!(a.rows(), 2);
assert!(a.get(0, 0).is_none());
assert!(a.get(0, 1).is_none());
assert!(a.get(1, 0).is_none());
assert!(a.get(1, 1).is_none());
}
#[test]
fn sparse_system_add_diag_accumulates() {
let mut sys = SparseSystem::new(2);
sys.add_diag(0, 1.0);
sys.add_diag(0, 2.0);
let a = sys.finish();
assert!((a.get(0, 0).unwrap() - 3.0).abs() < 1e-14);
}
#[test]
fn test_jacobi_preconditioner_extracts_diagonal() {
let mut tri = TriMat::new((4, 4));
tri.add_triplet(0, 0, 2.0);
tri.add_triplet(1, 1, 4.0);
tri.add_triplet(2, 2, 0.5);
tri.add_triplet(3, 3, 8.0);
tri.add_triplet(0, 1, 1.0);
tri.add_triplet(1, 0, 1.0);
tri.add_triplet(2, 3, -1.0);
tri.add_triplet(3, 2, -1.0);
let a = tri.to_csr();
let precond = jacobi_preconditioner(&a);
assert!((precond[0] - 0.5).abs() < 1e-14);
assert!((precond[1] - 0.25).abs() < 1e-14);
assert!((precond[2] - 2.0).abs() < 1e-14);
assert!((precond[3] - 0.125).abs() < 1e-14);
}
#[test]
fn test_jacobi_preconditioner_zero_diagonal_guard() {
let mut tri = TriMat::new((3, 3));
tri.add_triplet(0, 0, 2.0);
tri.add_triplet(1, 1, 0.0); tri.add_triplet(0, 1, 1.0);
tri.add_triplet(1, 0, 1.0);
let a = tri.to_csr();
let precond = jacobi_preconditioner(&a);
assert!((precond[0] - 0.5).abs() < 1e-14);
assert!((precond[1] - 1.0).abs() < 1e-14, "零对角元应守卫为 1.0");
assert!((precond[2] - 1.0).abs() < 1e-14, "缺失对角元应守卫为 1.0");
}
#[test]
fn test_pcg_fewer_iterations_than_plain_cg() {
let n = 20;
let mut tri = TriMat::new((n, n));
for i in 0..n {
tri.add_triplet(i, i, 2f64.powi(i as i32));
}
let a = tri.to_csr();
let b = vec![1.0; n];
let precond = jacobi_preconditioner(&a);
assert!(
conjugate_gradient(&a, &b, 5, 1e-10).is_none(),
"普通 CG 不应在 5 次迭代内收敛此病态系统"
);
let x_pcg = conjugate_gradient_preconditioned(&a, &b, &precond, 5, 1e-10)
.expect("PCG 应在 5 次迭代内收敛");
let ax = sparse_matvec(&a, &x_pcg);
for i in 0..n {
assert!(
(ax[i] - b[i]).abs() < 1e-8,
"分量 {i}: Ax={} b={}",
ax[i],
b[i]
);
}
}
#[test]
fn test_pcg_same_solution_as_plain_cg() {
let mut tri = TriMat::new((3, 3));
tri.add_triplet(0, 0, 2.0);
tri.add_triplet(1, 1, 3.0);
tri.add_triplet(2, 2, 2.0);
tri.add_triplet(0, 1, 1.0);
tri.add_triplet(1, 0, 1.0);
tri.add_triplet(1, 2, -1.0);
tri.add_triplet(2, 1, -1.0);
let a = tri.to_csr();
let b = vec![1.0, 0.0, 1.0];
let x_plain = conjugate_gradient(&a, &b, 100, 1e-12).unwrap();
let precond = jacobi_preconditioner(&a);
let x_pcg = conjugate_gradient_preconditioned(&a, &b, &precond, 100, 1e-12).unwrap();
for i in 0..3 {
assert!(
(x_plain[i] - x_pcg[i]).abs() < 1e-10,
"分量 {i}: 普通 CG={} PCG={}",
x_plain[i],
x_pcg[i]
);
}
}
#[test]
fn pcg_dimension_mismatch_returns_none() {
let mut tri = TriMat::new((2, 2));
tri.add_triplet(0, 0, 1.0);
tri.add_triplet(1, 1, 1.0);
let a = tri.to_csr();
let b = vec![1.0, 2.0];
let precond = vec![1.0, 2.0, 3.0]; assert!(conjugate_gradient_preconditioned(&a, &b, &precond, 10, 1e-10).is_none());
}
}