use crate::{
problems::elliptic::HodgeBlocks,
time::{LinearIrk, Tableau},
whitney_complex::HilbertComplex,
};
use crate::linalg::{faer::FaerCholesky, quadratic_form_sparse};
use derham::cochain::Cochain;
use exterior::ExteriorGrade;
use simplicial::{
geometry::metric::mesh::MeshLengthsSq,
linalg::{CooMatrix, CooMatrixExt, CsrMatrix, Vector},
};
pub struct WaveState {
pub pos: Vector,
pub vel: Vector,
}
impl WaveState {
pub fn new(pos: Vector, vel: Vector) -> Self {
Self { pos, vel }
}
pub fn energy<C: HilbertComplex>(&self, complex: &C, grade: ExteriorGrade) -> f64 {
let hb = HodgeBlocks::compute(complex, grade);
let inclusion = complex.inclusion(grade);
let u = inclusion.transpose() * &self.pos;
let w = inclusion.transpose() * &self.vel;
let sigma_energy = if hb.n_sigma > 0 {
let sigma = FaerCholesky::new(hb.mass_sigma.clone()).solve(&(hb.codif_dn() * &u));
quadratic_form_sparse(&hb.mass_sigma, &sigma)
} else {
0.0
};
0.5
* (sigma_energy
+ quadratic_form_sparse(&hb.stiff(), &u)
+ quadratic_form_sparse(&hb.mass_u, &w))
}
}
pub fn solve_wave<C: HilbertComplex>(
complex: &C,
grade: ExteriorGrade,
times: &[f64],
initial: WaveState,
force_data: Cochain,
) -> Vec<WaveState> {
let hb = HodgeBlocks::compute(complex, grade);
let (ns, nu) = (hb.n_sigma, hb.n_u);
let coo = CooMatrix::from;
let z = CooMatrix::zeros;
let mass_u = coo(&hb.mass_u);
let mass_block = CsrMatrix::from(&CooMatrix::block(&[
&[&z(ns, ns), &z(ns, nu), &z(ns, nu)],
&[&z(nu, ns), &mass_u, &z(nu, nu)],
&[&z(nu, ns), &z(nu, nu), &mass_u],
]));
let op_block = CsrMatrix::from(&CooMatrix::block(&[
&[&coo(&(-&hb.mass_sigma)), &coo(&hb.codif_dn()), &z(ns, nu)],
&[&z(nu, ns), &z(nu, nu), &mass_u],
&[&coo(&(-&hb.dif_sigma())), &coo(&(-&hb.stiff())), &z(nu, nu)],
]));
let inclusion = complex.inclusion(grade);
let u0 = inclusion.transpose() * initial.pos;
let w0 = inclusion.transpose() * initial.vel;
let sigma0 = if ns > 0 {
FaerCholesky::new(hb.mass_sigma.clone()).solve(&(hb.codif_dn() * &u0))
} else {
Vector::zeros(0)
};
let force = &hb.mass_u * (inclusion.transpose() * force_data.coeffs());
let mut forcing = Vector::zeros(ns + 2 * nu);
forcing.rows_mut(ns + nu, nu).copy_from(&force);
let dt = times.windows(2).next().map_or(0.0, |w| w[1] - w[0]);
let irk = LinearIrk::new(Tableau::gauss_legendre(2), &mass_block, op_block, dt);
let mut y = Vector::zeros(ns + 2 * nu);
y.rows_mut(0, ns).copy_from(&sigma0);
y.rows_mut(ns, nu).copy_from(&u0);
y.rows_mut(ns + nu, nu).copy_from(&w0);
let reconstruct = |y: &Vector| WaveState {
pos: &inclusion * y.rows(ns, nu),
vel: &inclusion * y.rows(ns + nu, nu),
};
let mut solution = Vec::with_capacity(times.len());
solution.push(reconstruct(&y));
for t01 in times.windows(2) {
let [t0, _t1] = t01 else { unreachable!() };
y = irk.step(&y, *t0, |_| forcing.clone());
solution.push(reconstruct(&y));
}
solution
}
pub fn cfl_dt(mesh_geo: &MeshLengthsSq, vel: f64) -> f64 {
const MARGIN: f64 = 0.95;
MARGIN * mesh_geo.mesh_width_min() / vel
}
#[cfg(test)]
mod test {
use super::*;
use crate::whitney_complex::WhitneyComplex;
use simplicial::gen::cartesian::CartesianGrid;
use approx::assert_relative_eq;
#[test]
fn energy_conserved_at_every_grade() {
for dim in 2..=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 n = whitney.ndofs(grade);
let pos = Vector::from_fn(n, |i, _| ((7 * i + 3) % 11) as f64 - 5.0);
let vel = Vector::from_fn(n, |i, _| ((4 * i + 1) % 9) as f64 - 4.0);
let force = Cochain::new(grade, Vector::zeros(n));
let times: Vec<f64> = (0..=100).map(|i| 0.1 * i as f64).collect();
let sol = solve_wave(&whitney, grade, ×, WaveState::new(pos, vel), force);
let energy0 = sol[0].energy(&whitney, grade);
for state in &sol {
let energy = state.energy(&whitney, grade);
assert_relative_eq!(energy, energy0, epsilon = 1e-8 * energy0.max(1.0));
}
}
}
}
}