use crate::ldu_matrix::fv_matrix::{SolverPerformance, SolverSettings};
use crate::ldu_matrix::ldu_matrix::LduMatrix;
use crate::ldu_matrix::solvers::gauss_seidel::gauss_seidel;
use crate::matrix::SquareMatrix;
use std::collections::HashMap;
const N_CELLS_COARSEST: usize = 10;
const MAX_LEVELS: usize = 50;
const N_PRE_SWEEPS: usize = 2;
const N_POST_SWEEPS: usize = 2;
struct GamgLevel {
restrict_map: Vec<usize>,
n_coarse: usize,
matrix: LduMatrix,
}
struct GamgCycle {
levels: Vec<GamgLevel>,
}
impl GamgCycle {
fn build(finest: &LduMatrix) -> GamgCycle {
let mut levels: Vec<GamgLevel> = Vec::new();
let mut forward = true;
loop {
let current: &LduMatrix = match levels.last() {
Some(l) => &l.matrix,
None => finest,
};
if current.n_cells <= N_CELLS_COARSEST || levels.len() >= MAX_LEVELS {
break;
}
let weights: Vec<f64> = current.upper.iter().map(|u| u.abs()).collect();
let (map, n_coarse) = pair_agglomerate(current, &weights, forward);
forward = !forward;
if n_coarse >= current.n_cells {
break;
}
let coarse = agglomerate_matrix(current, &map, n_coarse);
levels.push(GamgLevel {
restrict_map: map,
n_coarse,
matrix: coarse,
});
}
GamgCycle { levels }
}
fn solve_level(&self, mat: &LduMatrix, x: &mut Vec<f64>, b: &[f64], next_level: usize) {
if next_level >= self.levels.len() {
*x = solve_coarsest(mat, b);
return;
}
smooth(mat, b, x, N_PRE_SWEEPS);
let r = mat.residual(x, b); let lvl = &self.levels[next_level];
let r_coarse = restrict_field(&r, &lvl.restrict_map, lvl.n_coarse);
let mut e_coarse = vec![0.0_f64; lvl.n_coarse];
self.solve_level(&lvl.matrix, &mut e_coarse, &r_coarse, next_level + 1);
let correction = prolong_field(&e_coarse, &lvl.restrict_map);
let ac = mat.multiply(&correction);
let denom = dot(&correction, &ac);
let sf = if denom.abs() > 1e-300 {
dot(&r, &correction) / denom
} else {
1.0
};
for (xi, ci) in x.iter_mut().zip(&correction) {
*xi += sf * ci;
}
smooth(mat, b, x, N_POST_SWEEPS);
}
}
pub fn gamg(
ldu: &LduMatrix,
b: &[f64],
x0: Option<&[f64]>,
settings: &SolverSettings,
) -> (Vec<f64>, SolverPerformance) {
let n = ldu.n_cells;
debug_assert_eq!(b.len(), n);
let mut x = match x0 {
Some(g) => {
debug_assert_eq!(g.len(), n);
g.to_vec()
}
None => vec![0.0_f64; n],
};
let cycle = GamgCycle::build(ldu);
if cycle.levels.is_empty() {
let x = solve_coarsest(ldu, b);
let r = ldu.residual(&x, b);
let b_norm = l2(b).max(1e-300);
let final_residual = l2(&r) / b_norm;
return (
x,
SolverPerformance {
n_iterations: 1,
final_residual,
converged: true,
},
);
}
let b_norm = l2(b).max(1e-300);
let mut final_residual = l2(&ldu.residual(&x, b)) / b_norm;
if final_residual < settings.tolerance {
return (
x,
SolverPerformance {
n_iterations: 0,
final_residual,
converged: true,
},
);
}
let mut n_iter = 0;
while n_iter < settings.max_iter && final_residual >= settings.tolerance {
cycle.solve_level(ldu, &mut x, b, 0);
final_residual = l2(&ldu.residual(&x, b)) / b_norm;
n_iter += 1;
}
(
x,
SolverPerformance {
n_iterations: n_iter,
final_residual,
converged: final_residual < settings.tolerance,
},
)
}
fn pair_agglomerate(ldu: &LduMatrix, weights: &[f64], forward: bool) -> (Vec<usize>, usize) {
let n = ldu.n_cells;
let nf = ldu.n_internal_faces;
let mut n_nbrs = vec![0usize; n];
for f in 0..nf {
n_nbrs[ldu.owner[f]] += 1;
n_nbrs[ldu.neighbour[f]] += 1;
}
let mut offsets = vec![0usize; n + 1];
for c in 0..n {
offsets[c + 1] = offsets[c] + n_nbrs[c];
}
let mut cell_faces = vec![0usize; offsets[n]];
let mut fill = vec![0usize; n];
for f in 0..nf {
let o = ldu.owner[f];
let nb = ldu.neighbour[f];
cell_faces[offsets[o] + fill[o]] = f;
fill[o] += 1;
cell_faces[offsets[nb] + fill[nb]] = f;
fill[nb] += 1;
}
let mut map = vec![-1isize; n];
let mut n_coarse: isize = 0;
for cellfi in 0..n {
let celli = if forward { cellfi } else { n - cellfi - 1 };
if map[celli] >= 0 {
continue;
}
let mut match_face: isize = -1;
let mut max_w = f64::NEG_INFINITY;
for &f in &cell_faces[offsets[celli]..offsets[celli + 1]] {
if map[ldu.owner[f]] < 0 && map[ldu.neighbour[f]] < 0 && weights[f] > max_w {
match_face = f as isize;
max_w = weights[f];
}
}
if match_face >= 0 {
let f = match_face as usize;
map[ldu.owner[f]] = n_coarse;
map[ldu.neighbour[f]] = n_coarse;
n_coarse += 1;
} else {
let mut cl_face: isize = -1;
let mut cl_w = f64::NEG_INFINITY;
for &f in &cell_faces[offsets[celli]..offsets[celli + 1]] {
if weights[f] > cl_w {
cl_face = f as isize;
cl_w = weights[f];
}
}
if cl_face >= 0 {
let f = cl_face as usize;
map[celli] = map[ldu.owner[f]].max(map[ldu.neighbour[f]]);
}
}
}
for cellfi in 0..n {
let celli = if forward { cellfi } else { n - cellfi - 1 };
if map[celli] < 0 {
map[celli] = n_coarse;
n_coarse += 1;
}
}
if !forward {
n_coarse -= 1;
for m in map.iter_mut() {
*m = n_coarse - *m;
}
n_coarse += 1;
}
(
map.into_iter().map(|m| m as usize).collect(),
n_coarse as usize,
)
}
fn agglomerate_matrix(fine: &LduMatrix, map: &[usize], n_coarse: usize) -> LduMatrix {
let nf = fine.n_internal_faces;
let mut coarse_diag = vec![0.0_f64; n_coarse];
for c in 0..fine.n_cells {
coarse_diag[map[c]] += fine.diag[c];
}
let mut coarse_owner: Vec<usize> = Vec::new();
let mut coarse_neighbour: Vec<usize> = Vec::new();
let mut face_map: HashMap<(usize, usize), usize> = HashMap::new();
let mut face_restrict = vec![0isize; nf];
for f in 0..nf {
let co = map[fine.owner[f]];
let cn = map[fine.neighbour[f]];
if co == cn {
face_restrict[f] = -((co as isize) + 1);
} else {
let key = if co < cn { (co, cn) } else { (cn, co) };
let cf = *face_map.entry(key).or_insert_with(|| {
coarse_owner.push(key.0);
coarse_neighbour.push(key.1);
coarse_owner.len() - 1
});
face_restrict[f] = cf as isize;
}
}
let n_cf = coarse_owner.len();
let mut coarse_upper = vec![0.0_f64; n_cf];
for f in 0..nf {
let cf = face_restrict[f];
if cf >= 0 {
coarse_upper[cf as usize] += fine.upper[f];
} else {
coarse_diag[(-1 - cf) as usize] += 2.0 * fine.upper[f];
}
}
let mut m = LduMatrix::new(n_coarse, coarse_owner, coarse_neighbour);
m.diag = coarse_diag;
m.lower = coarse_upper.clone();
m.upper = coarse_upper;
m
}
fn restrict_field(fine: &[f64], map: &[usize], n_coarse: usize) -> Vec<f64> {
let mut cf = vec![0.0_f64; n_coarse];
for (i, &c) in map.iter().enumerate() {
cf[c] += fine[i];
}
cf
}
fn prolong_field(coarse: &[f64], map: &[usize]) -> Vec<f64> {
map.iter().map(|&c| coarse[c]).collect()
}
fn smooth(a: &LduMatrix, source: &[f64], x: &mut Vec<f64>, n_sweeps: usize) {
if n_sweeps == 0 {
return;
}
gauss_seidel(a, source, x, 0.0, n_sweeps);
}
fn solve_coarsest(a: &LduMatrix, source: &[f64]) -> Vec<f64> {
let n = a.n_cells;
let mut dense = SquareMatrix::new(n);
for c in 0..n {
dense.add(c, c, a.diag[c]);
}
for f in 0..a.n_internal_faces {
let o = a.owner[f];
let nb = a.neighbour[f];
dense.add(o, nb, a.upper[f]);
dense.add(nb, o, a.lower[f]);
}
match dense.solve(source) {
Ok(x) => x,
Err(_) => {
let mut x = vec![0.0_f64; n];
gauss_seidel(a, source, &mut x, 1e-12, 1000);
x
}
}
}
#[inline]
fn l2(v: &[f64]) -> f64 {
v.iter().map(|x| x * x).sum::<f64>().sqrt()
}
#[inline]
fn dot(a: &[f64], b: &[f64]) -> f64 {
a.iter().zip(b).map(|(x, y)| x * y).sum()
}
#[cfg(test)]
mod tests {
use super::*;
fn poisson_1d(n: usize) -> (LduMatrix, Vec<f64>) {
let h = 1.0 / (n + 1) as f64;
let owner: Vec<usize> = (0..n - 1).collect();
let neighbour: Vec<usize> = (1..n).collect();
let mut m = LduMatrix::new(n, owner, neighbour);
let c = 1.0 / (h * h);
m.diag = vec![2.0 * c; n];
m.upper = vec![-c; n - 1];
m.lower = vec![-c; n - 1];
(m, vec![1.0; n])
}
#[test]
fn gamg_solves_1d_poisson() {
let n = 200;
let h = 1.0 / (n + 1) as f64;
let (m, b) = poisson_1d(n);
let settings = SolverSettings {
tolerance: 1e-9,
max_iter: 100,
};
let (x, perf) = gamg(&m, &b, None, &settings);
assert!(
perf.converged,
"GAMG did not converge: res = {}",
perf.final_residual
);
for i in 0..n {
let xi = (i + 1) as f64 * h;
let exact = xi * (1.0 - xi) / 2.0;
assert!((x[i] - exact).abs() < 1e-3, "cell {i}: {} vs {exact}", x[i]);
}
}
#[test]
fn gamg_cycle_count_is_mesh_independent() {
let settings = SolverSettings {
tolerance: 1e-8,
max_iter: 200,
};
let (m1, b1) = poisson_1d(250);
let (m2, b2) = poisson_1d(2000);
let (_, p1) = gamg(&m1, &b1, None, &settings);
let (_, p2) = gamg(&m2, &b2, None, &settings);
assert!(p1.converged && p2.converged);
assert!(
p2.n_iterations <= (p1.n_iterations * 3) / 2 + 2,
"cycle count grew with mesh: {} → {}",
p1.n_iterations,
p2.n_iterations
);
}
#[test]
fn gamg_warm_start_returns_immediately() {
let (m, b) = poisson_1d(200);
let settings = SolverSettings {
tolerance: 1e-8,
max_iter: 100,
};
let (x, _) = gamg(&m, &b, None, &settings);
let (_, perf) = gamg(&m, &b, Some(&x), &settings);
assert_eq!(perf.n_iterations, 0);
assert!(perf.converged);
}
#[test]
fn gamg_handles_tiny_matrix() {
let mut m = LduMatrix::new(3, vec![0, 1], vec![1, 2]);
m.diag = vec![2.0, 2.0, 2.0];
m.upper = vec![-1.0, -1.0];
m.lower = vec![-1.0, -1.0];
let b = vec![1.0, 1.0, 1.0];
let settings = SolverSettings {
tolerance: 1e-10,
max_iter: 50,
};
let (x, perf) = gamg(&m, &b, None, &settings);
assert!(perf.converged);
assert!((x[0] - 1.5).abs() < 1e-9);
assert!((x[1] - 2.0).abs() < 1e-9);
assert!((x[2] - 1.5).abs() < 1e-9);
}
}