use core::marker::PhantomData;
use nalgebra::DVector;
use sprs::{CsMat, TriMat};
use cartan_core::Manifold;
use cartan_manifolds::euclidean::Euclidean;
use crate::error::DecError;
use crate::exterior::ExteriorDerivative;
use crate::hodge::HodgeStar;
use crate::mesh::{FlatMesh, Mesh};
pub struct Operators<M: Manifold = Euclidean<2>, const K: usize = 3, const B: usize = 2> {
pub laplace_beltrami: CsMat<f64>,
pub mass: Vec<DVector<f64>>,
pub ext: ExteriorDerivative,
pub hodge: HodgeStar,
pub mass0: DVector<f64>,
pub mass1: DVector<f64>,
_phantom: PhantomData<M>,
}
fn assemble_scalar_laplacian(ext: &ExteriorDerivative, hodge: &HodgeStar) -> CsMat<f64> {
let d0 = ext.d0();
let s1 = hodge.star1();
let ne = s1.len();
let mut star1_tri = TriMat::new((ne, ne));
for e in 0..ne {
let w = s1[e];
if w.abs() > 1e-30 {
star1_tri.add_triplet(e, e, w);
}
}
let star1_diag = star1_tri.to_csc();
let star1_d0 = &star1_diag * d0;
let d0t = d0.transpose_view();
let d0t_star1_d0 = &d0t * &star1_d0;
let nv = hodge.star0().len();
let star0_inv = hodge.star0_inv();
let mut star0_inv_tri = TriMat::new((nv, nv));
for v in 0..nv {
let w = star0_inv[v];
if w.abs() > 1e-30 {
star0_inv_tri.add_triplet(v, v, w);
}
}
let star0_inv_diag = star0_inv_tri.to_csc();
&star0_inv_diag * &d0t_star1_d0
}
impl Operators<Euclidean<2>, 3, 2> {
pub fn from_mesh(mesh: &FlatMesh, manifold: &Euclidean<2>) -> Self {
let ext = ExteriorDerivative::from_mesh(mesh);
let hodge = HodgeStar::from_mesh(mesh, manifold);
let laplace_beltrami = assemble_scalar_laplacian(&ext, &hodge);
Self {
laplace_beltrami,
mass0: hodge.star0().clone(),
mass1: hodge.star1().clone(),
mass: hodge.star.to_vec(),
ext,
hodge,
_phantom: PhantomData,
}
}
}
impl<M: Manifold, const K: usize, const B: usize> Operators<M, K, B> {
pub fn from_mesh_generic(
mesh: &Mesh<M, K, B>,
manifold: &M,
) -> Result<Self, DecError> {
let ext = ExteriorDerivative::from_mesh_sparse_generic(mesh);
let hodge = HodgeStar::from_mesh_generic(mesh, manifold)?;
let laplace_beltrami = assemble_scalar_laplacian(&ext, &hodge);
Ok(Self {
laplace_beltrami,
mass0: hodge.star0().clone(),
mass1: hodge.star1().clone(),
mass: hodge.star.to_vec(),
ext,
hodge,
_phantom: PhantomData,
})
}
pub fn apply_laplace_beltrami(&self, f: &DVector<f64>) -> DVector<f64> {
let n = f.len();
let mut result = DVector::<f64>::zeros(n);
for (col_idx, col) in self.laplace_beltrami.outer_iterator().enumerate() {
let x_j = f[col_idx];
for (row_idx, &val) in col.iter() {
result[row_idx] += val * x_j;
}
}
result
}
}
impl<M: Manifold> Operators<M, 3, 2> {
pub fn apply_bochner_laplacian(
&self,
u: &DVector<f64>,
ricci_correction: Option<&dyn Fn(usize) -> [[f64; 2]; 2]>,
) -> DVector<f64> {
let nv = self.laplace_beltrami.rows();
assert_eq!(u.len(), 2 * nv, "Bochner: u must have 2*n_v entries");
let ux = u.rows(0, nv).into_owned();
let uy = u.rows(nv, nv).into_owned();
let mut lux = self.apply_laplace_beltrami(&ux);
let mut luy = self.apply_laplace_beltrami(&uy);
if let Some(ric) = ricci_correction {
for v in 0..nv {
let r = ric(v);
let ux_v = ux[v];
let uy_v = uy[v];
lux[v] += r[0][0] * ux_v + r[0][1] * uy_v;
luy[v] += r[1][0] * ux_v + r[1][1] * uy_v;
}
}
let mut result = DVector::<f64>::zeros(2 * nv);
result.rows_mut(0, nv).copy_from(&lux);
result.rows_mut(nv, nv).copy_from(&luy);
result
}
pub fn apply_lichnerowicz_laplacian(
&self,
q: &DVector<f64>,
curvature_correction: Option<&dyn Fn(usize) -> [[f64; 3]; 3]>,
) -> DVector<f64> {
let nv = self.laplace_beltrami.rows();
assert_eq!(q.len(), 3 * nv, "Lichnerowicz: q must have 3*n_v entries");
let qxx = q.rows(0, nv).into_owned();
let qxy = q.rows(nv, nv).into_owned();
let qyy = q.rows(2 * nv, nv).into_owned();
let mut lxx = self.apply_laplace_beltrami(&qxx);
let mut lxy = self.apply_laplace_beltrami(&qxy);
let mut lyy = self.apply_laplace_beltrami(&qyy);
if let Some(curv) = curvature_correction {
for v in 0..nv {
let c = curv(v);
let qx = qxx[v];
let qm = qxy[v];
let qy = qyy[v];
lxx[v] += c[0][0] * qx + c[0][1] * qm + c[0][2] * qy;
lxy[v] += c[1][0] * qx + c[1][1] * qm + c[1][2] * qy;
lyy[v] += c[2][0] * qx + c[2][1] * qm + c[2][2] * qy;
}
}
let mut result = DVector::<f64>::zeros(3 * nv);
result.rows_mut(0, nv).copy_from(&lxx);
result.rows_mut(nv, nv).copy_from(&lxy);
result.rows_mut(2 * nv, nv).copy_from(&lyy);
result
}
}
#[cfg(test)]
mod tests {
use super::*;
use cartan_manifolds::euclidean::Euclidean;
#[test]
fn test_bochner_tensor_ricci_zero() {
let mesh = FlatMesh::unit_square_grid(4);
let ops = Operators::from_mesh(&mesh, &Euclidean::<2>);
let nv = mesh.n_vertices();
let u = DVector::from_element(2 * nv, 0.5);
let result_none = ops.apply_bochner_laplacian(&u, None);
let result_zero = ops.apply_bochner_laplacian(&u, Some(&|_| [[0.0, 0.0], [0.0, 0.0]]));
let diff = (&result_none - &result_zero).norm();
assert!(
diff < 1e-12,
"zero callback should equal None: diff = {diff}"
);
}
#[test]
fn test_bochner_tensor_ricci_einstein() {
let mesh = FlatMesh::unit_square_grid(4);
let ops = Operators::from_mesh(&mesh, &Euclidean::<2>);
let nv = mesh.n_vertices();
let kappa = 2.5;
let u = DVector::from_fn(2 * nv, |i, _| i as f64 * 0.01);
let result_tensor =
ops.apply_bochner_laplacian(&u, Some(&|_| [[kappa, 0.0], [0.0, kappa]]));
let ux = u.rows(0, nv).into_owned();
let uy = u.rows(nv, nv).into_owned();
let lux = ops.apply_laplace_beltrami(&ux) + &ux * kappa;
let luy = ops.apply_laplace_beltrami(&uy) + &uy * kappa;
let mut expected = DVector::zeros(2 * nv);
expected.rows_mut(0, nv).copy_from(&lux);
expected.rows_mut(nv, nv).copy_from(&luy);
let diff = (&result_tensor - &expected).norm();
assert!(
diff < 1e-10,
"Einstein tensor != scalar path: diff = {diff}"
);
}
#[test]
fn test_lichnerowicz_tensor_callback_zero() {
let mesh = FlatMesh::unit_square_grid(4);
let ops = Operators::from_mesh(&mesh, &Euclidean::<2>);
let nv = mesh.n_vertices();
let q = DVector::from_element(3 * nv, 0.3);
let result_none = ops.apply_lichnerowicz_laplacian(&q, None);
let result_zero = ops.apply_lichnerowicz_laplacian(&q, Some(&|_| [[0.0_f64; 3]; 3]));
let diff = (&result_none - &result_zero).norm();
assert!(
diff < 1e-12,
"zero callback should match None: diff = {diff}"
);
}
#[test]
fn test_lichnerowicz_tensor_callback_diagonal() {
let mesh = FlatMesh::unit_square_grid(4);
let ops = Operators::from_mesh(&mesh, &Euclidean::<2>);
let nv = mesh.n_vertices();
let kappa = 1.0;
let q = DVector::from_fn(3 * nv, |i, _| (i + 1) as f64 * 0.01);
let c = [
[2.0 * kappa, 0., 0.],
[0., 2.0 * kappa, 0.],
[0., 0., 2.0 * kappa],
];
let result_tensor = ops.apply_lichnerowicz_laplacian(&q, Some(&|_| c));
let qxx = q.rows(0, nv).into_owned();
let qxy = q.rows(nv, nv).into_owned();
let qyy = q.rows(2 * nv, nv).into_owned();
let lxx = ops.apply_laplace_beltrami(&qxx) + &qxx * (2.0 * kappa);
let lxy = ops.apply_laplace_beltrami(&qxy) + &qxy * (2.0 * kappa);
let lyy = ops.apply_laplace_beltrami(&qyy) + &qyy * (2.0 * kappa);
let mut expected = DVector::zeros(3 * nv);
expected.rows_mut(0, nv).copy_from(&lxx);
expected.rows_mut(nv, nv).copy_from(&lxy);
expected.rows_mut(2 * nv, nv).copy_from(&lyy);
let diff = (&result_tensor - &expected).norm();
assert!(
diff < 1e-10,
"diagonal tensor != scalar path: diff = {diff}"
);
}
}