use crate::linalg::faer::FaerCholesky;
use simplicial::linalg::{CooMatrix, CooMatrixExt, CsrMatrix, Vector};
use crate::{
problems::elliptic::HodgeBlocks,
time::{LinearIrk, Tableau},
whitney_complex::HilbertComplex,
};
use derham::cochain::Cochain;
use exterior::ExteriorGrade;
pub fn solve_heat<C: HilbertComplex>(
complex: &C,
grade: ExteriorGrade,
nsteps: usize,
dt: f64,
initial: &Cochain,
source: &Cochain,
diffusion_coeff: f64,
) -> Vec<Cochain> {
let hb = HodgeBlocks::compute(complex, grade);
let (ns, nu) = (hb.n_sigma, hb.n_u);
let coo = CooMatrix::from;
let mass_block = CsrMatrix::from(&CooMatrix::block(&[
&[&CooMatrix::zeros(ns, ns), &CooMatrix::zeros(ns, nu)],
&[&CooMatrix::zeros(nu, ns), &coo(&hb.mass_u)],
]));
let op_block = CsrMatrix::from(&CooMatrix::block(&[
&[&coo(&(-&hb.mass_sigma)), &coo(&hb.codif_dn())],
&[
&coo(&(-diffusion_coeff * &hb.dif_sigma())),
&coo(&(-diffusion_coeff * &hb.stiff())),
],
]));
let inclusion = complex.inclusion(grade);
let u0 = inclusion.transpose() * initial.coeffs();
let sigma0 = if ns > 0 {
FaerCholesky::new(hb.mass_sigma.clone()).solve(&(hb.codif_dn() * &u0))
} else {
Vector::zeros(0)
};
let source_u = &hb.mass_u * (inclusion.transpose() * source.coeffs());
let mut forcing = Vector::zeros(ns + nu);
forcing.rows_mut(ns, nu).copy_from(&source_u);
let irk = LinearIrk::new(Tableau::radau_iia(2), &mass_block, op_block, dt);
let mut y = Vector::zeros(ns + nu);
y.rows_mut(0, ns).copy_from(&sigma0);
y.rows_mut(ns, nu).copy_from(&u0);
let mut solution = Vec::with_capacity(nsteps + 1);
solution.push(Cochain::new(grade, &inclusion * &u0));
for istep in 0..nsteps {
y = irk.step(&y, istep as f64 * dt, |_| forcing.clone());
let u = &inclusion * y.rows(ns, nu);
solution.push(Cochain::new(grade, u));
}
solution
}
#[cfg(test)]
mod test {
use super::*;
use crate::linalg::quadratic_form_sparse;
use crate::problems::elliptic::solve_source;
use crate::whitney_complex::WhitneyComplex;
use simplicial::gen::cartesian::CartesianGrid;
use approx::assert_relative_eq;
#[test]
fn energy_dissipates_at_every_grade() {
for dim in 1..=3 {
let (topology, coords) = CartesianGrid::new_unit(dim, 2).triangulate();
let metric = coords.to_edge_lengths_sq(&topology);
let whitney = WhitneyComplex::new(&topology, &metric);
for grade in 0..=dim {
let mass = CsrMatrix::from(&whitney.mass(grade));
let n = whitney.ndofs(grade);
let u0 = Cochain::new(
grade,
Vector::from_fn(n, |i, _| ((5 * i + 2) % 7) as f64 - 3.0),
);
let source = Cochain::new(grade, Vector::zeros(n));
let sol = solve_heat(&whitney, grade, 30, 0.05, &u0, &source, 1.0);
let mut prev = f64::INFINITY;
for u in &sol {
let energy = quadratic_form_sparse(&mass, u.coeffs());
assert!(
energy <= prev + 1e-9,
"energy must not increase (dim {dim}, grade {grade})"
);
prev = energy;
}
}
}
}
#[test]
fn steady_state_matches_static_hodge_laplace() {
let (topology, coords) = CartesianGrid::new_unit(2, 3).triangulate();
let metric = coords.to_edge_lengths_sq(&topology);
let whitney = WhitneyComplex::new(&topology, &metric);
let relative = whitney.relative();
let grade = 1;
assert_eq!(relative.harmonic_dim(grade), 0);
let n_rel = relative.ndofs(grade);
let f_rel = Vector::from_fn(n_rel, |i, _| ((3 * i + 1) % 5) as f64 - 2.0);
let inclusion = relative.inclusion(grade);
let source = Cochain::new(grade, &inclusion * &f_rel);
let mass_rel = CsrMatrix::from(&relative.mass(grade));
let galvec = &inclusion * (&mass_rel * &f_rel);
let (_sigma, u_static, _p) = solve_source(&relative, galvec, grade).expect("static solve");
let zero = Cochain::new(grade, Vector::zeros(whitney.ndofs(grade)));
let sol = solve_heat(&relative, grade, 200, 1.0, &zero, &source, 1.0);
let u_final = sol.last().unwrap();
assert_relative_eq!(u_final.coeffs(), u_static.coeffs(), epsilon = 1e-7);
}
}