use crate::{
linalg::{faer::FaerLu, quadratic_form_sparse},
time::{Leapfrog, LinearIrk, Tableau},
whitney_complex::{HilbertComplex, RelativeWhitneyComplex},
};
use derham::cochain::Cochain;
use exterior::ExteriorGrade;
use simplicial::{
linalg::{CooMatrix, CsrMatrix, Vector},
Dim,
};
#[derive(Clone)]
pub struct MixedField {
grades: Vec<Cochain>,
}
impl MixedField {
pub fn new(grades: Vec<Cochain>) -> Self {
for (k, c) in grades.iter().enumerate() {
assert_eq!(c.grade(), k, "slot k must hold a k-cochain");
}
Self { grades }
}
pub fn zeros<C: HilbertComplex>(complex: &C) -> Self {
let grades = (0..=complex.dim())
.map(|k| Cochain::new(k, Vector::zeros(complex.ndofs(k))))
.collect();
Self { grades }
}
pub fn from_grade<C: HilbertComplex>(complex: &C, u: Cochain) -> Self {
let k = u.grade();
assert_eq!(u.len(), complex.ndofs(k), "grade k cochain has wrong ndofs");
let mut field = Self::zeros(complex);
field.grades[k] = u;
field
}
pub fn dim(&self) -> Dim {
self.grades.len() - 1
}
pub fn grade(&self, k: ExteriorGrade) -> &Cochain {
&self.grades[k]
}
pub fn into_grades(self) -> Vec<Cochain> {
self.grades
}
}
pub struct HodgeDirac {
offsets: Vec<usize>,
masses: Vec<CsrMatrix>,
mass_block: CsrMatrix,
op: CsrMatrix,
}
impl HodgeDirac {
pub fn assemble<C: HilbertComplex>(complex: &C) -> Self {
Self::assemble_signed(complex, -1.0)
}
pub fn assemble_selfadjoint<C: HilbertComplex>(complex: &C) -> Self {
Self::assemble_signed(complex, 1.0)
}
fn assemble_signed<C: HilbertComplex>(complex: &C, delta_sign: f64) -> Self {
let dim = complex.dim();
let masses: Vec<CsrMatrix> = (0..=dim)
.map(|k| CsrMatrix::from(&complex.mass(k)))
.collect();
let difs: Vec<CsrMatrix> = (0..dim).map(|k| complex.dif(k)).collect();
let mut offsets = Vec::with_capacity(dim + 2);
let mut acc = 0;
for mass in &masses {
offsets.push(acc);
acc += mass.nrows();
}
offsets.push(acc);
let total = acc;
let mut mass_block = CooMatrix::new(total, total);
for (k, mass) in masses.iter().enumerate() {
let off = offsets[k];
for (r, c, &v) in mass.triplet_iter() {
mass_block.push(off + r, off + c, v);
}
}
let mut op = CooMatrix::new(total, total);
for k in 1..=dim {
let u_k = &masses[k] * &difs[k - 1];
let (row, col) = (offsets[k], offsets[k - 1]);
for (r, c, &v) in u_k.triplet_iter() {
op.push(row + r, col + c, v);
op.push(col + c, row + r, delta_sign * v);
}
}
Self {
offsets,
masses,
mass_block: CsrMatrix::from(&mass_block),
op: CsrMatrix::from(&op),
}
}
pub fn dim(&self) -> Dim {
self.offsets.len() - 2
}
pub fn ndofs_total(&self) -> usize {
*self.offsets.last().unwrap()
}
pub fn mass_block(&self) -> &CsrMatrix {
&self.mass_block
}
pub fn op(&self) -> &CsrMatrix {
&self.op
}
pub fn flatten(&self, field: &MixedField) -> Vector {
let mut y = Vector::zeros(self.ndofs_total());
for k in 0..=self.dim() {
let (off, n) = (self.offsets[k], self.offsets[k + 1] - self.offsets[k]);
y.rows_mut(off, n).copy_from(field.grade(k).coeffs());
}
y
}
pub fn unflatten(&self, y: &Vector) -> MixedField {
let grades = (0..=self.dim())
.map(|k| {
let (off, n) = (self.offsets[k], self.offsets[k + 1] - self.offsets[k]);
Cochain::new(k, y.rows(off, n).into_owned())
})
.collect();
MixedField::new(grades)
}
pub fn energy(&self, field: &MixedField) -> f64 {
0.5 * quadratic_form_sparse(&self.mass_block, &self.flatten(field))
}
pub fn grade_energy(&self, field: &MixedField, grade: ExteriorGrade) -> f64 {
0.5 * quadratic_form_sparse(&self.masses[grade], field.grade(grade).coeffs())
}
pub fn grade_parity_coloring(&self) -> Vec<bool> {
let mut color = vec![false; self.ndofs_total()];
for k in (1..=self.dim()).step_by(2) {
color[self.offsets[k]..self.offsets[k + 1]].fill(true);
}
color
}
}
fn restrict_field<C: HilbertComplex>(complex: &C, f: &MixedField) -> MixedField {
MixedField::new(
(0..=complex.dim())
.map(|k| Cochain::new(k, complex.inclusion(k).transpose() * f.grade(k).coeffs()))
.collect(),
)
}
fn extend_field<C: HilbertComplex>(complex: &C, f: &MixedField) -> MixedField {
MixedField::new(
(0..=complex.dim())
.map(|k| Cochain::new(k, &complex.inclusion(k) * f.grade(k).coeffs()))
.collect(),
)
}
pub fn solve_dirac<C: HilbertComplex>(
complex: &C,
times: &[f64],
initial: MixedField,
) -> Vec<MixedField> {
let dirac = HodgeDirac::assemble(complex);
let dt = times.windows(2).next().map_or(0.0, |w| w[1] - w[0]);
let irk = LinearIrk::new(
Tableau::gauss_legendre(2),
&dirac.mass_block,
dirac.op.clone(),
dt,
);
let mut y = dirac.flatten(&restrict_field(complex, &initial));
let mut solution = Vec::with_capacity(times.len());
solution.push(extend_field(complex, &dirac.unflatten(&y)));
for t01 in times.windows(2) {
let [t0, _t1] = t01 else { unreachable!() };
y = irk.step(&y, *t0, |_| Vector::zeros(dirac.ndofs_total()));
solution.push(extend_field(complex, &dirac.unflatten(&y)));
}
solution
}
pub fn solve_dirac_leapfrog<C: HilbertComplex>(
complex: &C,
times: &[f64],
initial: MixedField,
) -> Vec<MixedField> {
let dirac = HodgeDirac::assemble(complex);
let color = dirac.grade_parity_coloring();
let dt = times.windows(2).next().map_or(0.0, |w| w[1] - w[0]);
let leapfrog = Leapfrog::new(&dirac.mass_block, &dirac.op, &color, dt);
let mut y = dirac.flatten(&restrict_field(complex, &initial));
let mut solution = Vec::with_capacity(times.len());
solution.push(extend_field(complex, &dirac.unflatten(&y)));
for _ in times.windows(2) {
y = leapfrog.step(&y);
solution.push(extend_field(complex, &dirac.unflatten(&y)));
}
solution
}
pub fn solve_dirac_source(
relative: &RelativeWhitneyComplex,
mass_term: f64,
load: &MixedField,
boundary_values: &MixedField,
) -> MixedField {
let full = relative.full();
let dirac = HodgeDirac::assemble_selfadjoint(&full);
let n_full = dirac.ndofs_total();
let mut system = CooMatrix::new(n_full, n_full);
for (r, c, &v) in dirac.op.triplet_iter() {
system.push(r, c, v);
}
if mass_term != 0.0 {
for (r, c, &v) in dirac.mass_block.triplet_iter() {
system.push(r, c, mass_term * v);
}
}
let system = CsrMatrix::from(&system);
let n_relative: usize = (0..=relative.dim()).map(|k| relative.ndofs(k)).sum();
let mut inclusion = CooMatrix::new(n_full, n_relative);
let mut col_offset = 0;
for k in 0..=relative.dim() {
let e_k = relative.inclusion(k);
for (r, c, &v) in e_k.triplet_iter() {
inclusion.push(dirac.offsets[k] + r, col_offset + c, v);
}
col_offset += relative.ndofs(k);
}
let inclusion = CsrMatrix::from(&inclusion);
let lift = dirac.flatten(boundary_values);
let rhs = inclusion.transpose() * (dirac.flatten(load) - &system * &lift);
let system_relative = inclusion.transpose() * &system * &inclusion;
let interior = FaerLu::new(system_relative).solve(&rhs);
dirac.unflatten(&(lift + inclusion * interior))
}
#[cfg(test)]
mod test {
use super::*;
use crate::{
linalg::faer::FaerCholesky, problems::elliptic::HodgeBlocks, whitney_complex::WhitneyComplex,
};
use simplicial::gen::cartesian::CartesianGrid;
use approx::assert_relative_eq;
fn minkowski_mesh(
dim: Dim,
nsub: usize,
) -> (
simplicial::topology::complex::Complex,
simplicial::geometry::coord::mesh::MeshCoords,
) {
let (topology, coords) = CartesianGrid::new_unit(dim, nsub).triangulate();
let mut matrix = coords.into_matrix();
matrix.row_mut(0).scale_mut(0.7);
let spacetime = simplicial::geometry::coord::mesh::MeshCoords::with_ambient(
matrix,
gramian::Gramian::minkowski(dim),
);
(topology, spacetime)
}
fn seed_field(dirac: &HodgeDirac) -> MixedField {
let grades = (0..=dirac.dim())
.map(|k| {
let n = dirac.offsets[k + 1] - dirac.offsets[k];
Cochain::new(
k,
Vector::from_fn(n, |i, _| ((7 * i + 3 * k + 1) % 11) as f64 - 5.0),
)
})
.collect();
MixedField::new(grades)
}
#[test]
fn operator_is_skew_symmetric() {
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);
let dirac = HodgeDirac::assemble(&whitney);
let a = &dirac.op;
let skew = a + &a.transpose();
assert_relative_eq!(
skew.values().iter().fold(0.0, |m: f64, &v| m.max(v.abs())),
0.0
);
}
}
#[test]
fn dirac_squared_is_negative_hodge_laplacian() {
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);
let dirac = HodgeDirac::assemble(&whitney);
let chol: Vec<_> = (0..=dim)
.map(|k| FaerCholesky::new(dirac.masses[k].clone()))
.collect();
let apply_dirac = |u: &MixedField| {
let mv = &dirac.op * dirac.flatten(u);
let grades = (0..=dim)
.map(|k| {
let (off, n) = (dirac.offsets[k], dirac.offsets[k + 1] - dirac.offsets[k]);
Cochain::new(k, chol[k].solve(&mv.rows(off, n).into_owned()))
})
.collect();
MixedField::new(grades)
};
let u = seed_field(&dirac);
let d2u = apply_dirac(&apply_dirac(&u));
#[allow(clippy::needless_range_loop)] for grade in 0..=dim {
let hb = HodgeBlocks::compute(&whitney, grade);
let uk = u.grade(grade).coeffs();
let up = hb.stiff() * uk;
let dn = if hb.n_sigma > 0 {
let s = FaerCholesky::new(hb.mass_sigma.clone()).solve(&(&hb.codif_dn() * uk));
&hb.dif_sigma() * s
} else {
Vector::zeros(hb.n_u)
};
let lap = chol[grade].solve(&(up + dn));
let lhs = d2u.grade(grade).coeffs();
assert_relative_eq!(
(lhs + &lap).norm(),
0.0,
epsilon = 1e-9 * lap.norm().max(1.0)
);
}
}
}
#[test]
fn energy_conserved_at_every_dimension() {
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);
let dirac = HodgeDirac::assemble(&whitney);
let initial = seed_field(&dirac);
let times: Vec<f64> = (0..=100).map(|i| 0.05 * i as f64).collect();
let solution = solve_dirac(&whitney, ×, initial);
let energy0 = dirac.energy(&solution[0]);
assert!(energy0 > 0.0);
for state in &solution {
let energy = dirac.energy(state);
assert_relative_eq!(energy, energy0, epsilon = 1e-9 * energy0);
}
}
}
#[test]
fn leapfrog_conserves_staggered_energy_at_every_dimension() {
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);
let dirac = HodgeDirac::assemble(&whitney);
let color = dirac.grade_parity_coloring();
let dt = 0.1 * metric.mesh_width_min();
let leapfrog = Leapfrog::new(&dirac.mass_block, &dirac.op, &color, dt);
let mut y = dirac.flatten(&seed_field(&dirac));
let e0 = leapfrog.conserved_energy(&y);
assert!(e0 > 0.0);
for _ in 0..200 {
y = leapfrog.step(&y);
assert_relative_eq!(leapfrog.conserved_energy(&y), e0, epsilon = 1e-9 * e0);
}
}
}
#[test]
fn selfadjoint_operator_is_symmetric() {
for dim in 1..=3 {
let (topology, coords) = CartesianGrid::new_unit(dim, 2).triangulate();
let (_, spacetime) = minkowski_mesh(dim, 2);
let riemannian = coords.to_edge_lengths_sq(&topology);
let lorentzian = spacetime.to_edge_lengths_sq(&topology);
let check = |dirac: &HodgeDirac| {
let a = &dirac.op;
let sym = a - &a.transpose();
assert_relative_eq!(
sym.values().iter().fold(0.0, |m: f64, &v| m.max(v.abs())),
0.0
);
};
check(&HodgeDirac::assemble_selfadjoint(&WhitneyComplex::new(
&topology,
&riemannian,
)));
check(&HodgeDirac::assemble_selfadjoint(&WhitneyComplex::new(
&topology,
&lorentzian,
)));
}
}
#[test]
fn selfadjoint_dirac_squares_to_hodge_laplacian_on_minkowski() {
for dim in 1..=3 {
let (topology, spacetime) = minkowski_mesh(dim, 2);
let regge = spacetime.to_edge_lengths_sq(&topology);
let whitney = WhitneyComplex::new(&topology, ®ge);
let dirac = HodgeDirac::assemble_selfadjoint(&whitney);
let lu: Vec<_> = (0..=dim)
.map(|k| crate::linalg::faer::FaerLu::new(dirac.masses[k].clone()))
.collect();
let apply_dirac = |u: &MixedField| {
let mv = &dirac.op * dirac.flatten(u);
let grades = (0..=dim)
.map(|k| {
let (off, n) = (dirac.offsets[k], dirac.offsets[k + 1] - dirac.offsets[k]);
Cochain::new(k, lu[k].solve(&mv.rows(off, n).into_owned()))
})
.collect();
MixedField::new(grades)
};
let u = seed_field(&dirac);
let d2u = apply_dirac(&apply_dirac(&u));
#[allow(clippy::needless_range_loop)] for grade in 0..=dim {
let hb = HodgeBlocks::compute(&whitney, grade);
let uk = u.grade(grade).coeffs();
let up = hb.stiff() * uk;
let dn = if hb.n_sigma > 0 {
let s =
crate::linalg::faer::FaerLu::new(hb.mass_sigma.clone()).solve(&(&hb.codif_dn() * uk));
&hb.dif_sigma() * s
} else {
Vector::zeros(hb.n_u)
};
let lap = lu[grade].solve(&(up + dn));
let lhs = d2u.grade(grade).coeffs();
assert_relative_eq!(
(lhs - &lap).norm(),
0.0,
epsilon = 1e-9 * lap.norm().max(1.0)
);
}
}
}
#[test]
fn dirac_source_reproduces_constant_field() {
use simplicial::{
geometry::{coord::mesh::MeshCoords, metric::mesh::MeshLengthsSq},
topology::complex::Complex,
};
fn run(topology: &Complex, coords: &MeshCoords, geometry: &MeshLengthsSq) {
let dim = topology.dim();
let whitney = WhitneyComplex::new(topology, geometry);
let relative = whitney.relative();
let dirac = HodgeDirac::assemble_selfadjoint(&whitney);
let exact = MixedField::new(
(0..=dim)
.map(|k| {
let form = glatt::field::DiffFormClosure::new(
move |_: &coorder::Coord| {
exterior::MultiForm::new(
Vector::from_element(exterior::exterior_dim(dim, k), 1.0),
dim,
k,
)
},
dim,
k,
);
let field = derham::section::CoordFieldExt::pullback_on(&form, topology, coords);
derham::project::derham_map(&field, topology, 2)
})
.collect(),
);
let mass_term = 1.0;
let load = dirac.unflatten(&(&dirac.mass_block * dirac.flatten(&exact) * mass_term));
let solution = solve_dirac_source(&relative, mass_term, &load, &exact);
for k in 0..=dim {
assert_relative_eq!(
solution.grade(k).coeffs(),
exact.grade(k).coeffs(),
epsilon = 1e-9
);
}
}
for dim in 1..=3 {
let (topology, coords) = CartesianGrid::new_unit(dim, 2).triangulate();
let riemannian = coords.to_edge_lengths_sq(&topology);
run(&topology, &coords, &riemannian);
let (topology, spacetime) = minkowski_mesh(dim, 2);
let euclidean_view = MeshCoords::new(spacetime.matrix().clone());
run(
&topology,
&euclidean_view,
&spacetime.to_edge_lengths_sq(&topology),
);
}
}
#[test]
fn leapfrog_agrees_with_gauss_legendre() {
let (topology, coords) = CartesianGrid::new_unit(2, 2).triangulate();
let metric = coords.to_edge_lengths_sq(&topology);
let whitney = WhitneyComplex::new(&topology, &metric);
let dirac = HodgeDirac::assemble(&whitney);
let dt = 0.02 * metric.mesh_width_min();
let times: Vec<f64> = (0..=50).map(|i| dt * i as f64).collect();
let initial = seed_field(&dirac);
let implicit = solve_dirac(&whitney, ×, initial.clone());
let explicit = solve_dirac_leapfrog(&whitney, ×, initial);
let last_implicit = dirac.flatten(implicit.last().unwrap());
let last_explicit = dirac.flatten(explicit.last().unwrap());
let rel_err = (&last_implicit - &last_explicit).norm() / last_implicit.norm();
assert!(rel_err < 1e-2, "solvers disagree by {rel_err}");
}
}