use super::local_solver::{local_matrices, solve_local};
use super::{HdgConfig, HdgMesh};
use crate::error::{IntegrateError, IntegrateResult};
use scirs2_core::ndarray::Array2;
#[derive(Debug, Clone)]
pub struct HdgSolution {
pub trace_values: Vec<f64>,
pub volume_values: Vec<Vec<f64>>,
pub mesh: HdgMesh,
}
impl HdgSolution {
pub fn l2_error(&self, u_exact: &dyn Fn(f64, f64) -> f64) -> f64 {
use super::{jacobian_det, ref_to_physical, triangle_gauss_quadrature_3pt};
let (qps, wts) = triangle_gauss_quadrature_3pt();
let mut esq = 0.0_f64;
for (eidx, elem) in self.mesh.elements.iter().enumerate() {
let v: [[f64; 2]; 3] = [
self.mesh.vertices[elem[0]],
self.mesh.vertices[elem[1]],
self.mesh.vertices[elem[2]],
];
let det = jacobian_det(&v);
let uv = &self.volume_values[eidx];
for (qp, &w) in qps.iter().zip(wts.iter()) {
let xi = qp[0];
let eta = qp[1];
let ph = ref_to_physical(xi, eta, &v);
let uh = (1.0 - xi - eta) * uv[0] + xi * uv[1] + eta * uv[2];
let ue = u_exact(ph[0], ph[1]);
esq += (uh - ue).powi(2) * det * w;
}
}
esq.sqrt()
}
pub fn eval(&self, x: f64, y: f64) -> Option<f64> {
for (eidx, elem) in self.mesh.elements.iter().enumerate() {
let v = [
self.mesh.vertices[elem[0]],
self.mesh.vertices[elem[1]],
self.mesh.vertices[elem[2]],
];
let d = (v[1][1] - v[2][1]) * (v[0][0] - v[2][0])
+ (v[2][0] - v[1][0]) * (v[0][1] - v[2][1]);
if d.abs() < 1e-14 {
continue;
}
let l0 =
((v[1][1] - v[2][1]) * (x - v[2][0]) + (v[2][0] - v[1][0]) * (y - v[2][1])) / d;
let l1 =
((v[2][1] - v[0][1]) * (x - v[2][0]) + (v[0][0] - v[2][0]) * (y - v[2][1])) / d;
let l2 = 1.0 - l0 - l1;
if l0 >= -1e-10 && l1 >= -1e-10 && l2 >= -1e-10 {
let uv = &self.volume_values[eidx];
return Some(l0 * uv[0] + l1 * uv[1] + l2 * uv[2]);
}
}
None
}
}
fn lu_solve(a: &mut Array2<f64>, b: &mut [f64]) -> IntegrateResult<Vec<f64>> {
let n = a.nrows();
if n != a.ncols() || n != b.len() {
return Err(IntegrateError::DimensionMismatch(
"LU: bad dimensions".into(),
));
}
for col in 0..n {
let (mut mv, mut mr) = (a[[col, col]].abs(), col);
for row in (col + 1)..n {
if a[[row, col]].abs() > mv {
mv = a[[row, col]].abs();
mr = row;
}
}
if mv < 1e-15 {
return Err(IntegrateError::LinearSolveError(format!(
"Singular at col {col}"
)));
}
if mr != col {
for k in 0..n {
let t = a[[col, k]];
a[[col, k]] = a[[mr, k]];
a[[mr, k]] = t;
}
b.swap(col, mr);
}
let p = a[[col, col]];
for row in (col + 1)..n {
let f = a[[row, col]] / p;
a[[row, col]] = f;
for k in (col + 1)..n {
let s = f * a[[col, k]];
a[[row, k]] -= s;
}
b[row] -= f * b[col];
}
}
let mut x = vec![0.0_f64; n];
for i in (0..n).rev() {
let mut s = b[i];
for j in (i + 1)..n {
s -= a[[i, j]] * x[j];
}
x[i] = s / a[[i, i]];
}
Ok(x)
}
fn apply_dirichlet(
mat: &mut Array2<f64>,
rhs: &mut [f64],
mesh: &HdgMesh,
g: &dyn Fn(f64, f64) -> f64,
) {
let n = mat.nrows();
let mut bv = std::collections::BTreeSet::new();
for &fi in &mesh.boundary_faces {
bv.insert(mesh.faces[fi][0]);
bv.insert(mesh.faces[fi][1]);
}
let mut bc = vec![f64::NAN; n];
for &v in &bv {
bc[v] = g(mesh.vertices[v][0], mesh.vertices[v][1]);
}
for &v in &bv {
let gv = bc[v];
for row in 0..n {
if bc[row].is_nan() {
rhs[row] -= mat[[row, v]] * gv;
}
}
}
for &v in &bv {
for j in 0..n {
mat[[v, j]] = 0.0;
mat[[j, v]] = 0.0;
}
mat[[v, v]] = 1.0;
rhs[v] = bc[v];
}
}
pub fn solve_hdg(
mesh: HdgMesh,
f: &dyn Fn(f64, f64) -> f64,
g: &dyn Fn(f64, f64) -> f64,
config: HdgConfig,
) -> IntegrateResult<HdgSolution> {
let tau = config.tau_stabilization;
let n_v = mesh.vertices.len();
let n_e = mesh.n_elements();
let mut lmats = Vec::with_capacity(n_e);
for ei in 0..n_e {
lmats.push(local_matrices(ei, &mesh, tau, f)?);
}
let mut gmat = Array2::<f64>::zeros((n_v, n_v));
let mut grhs = vec![0.0_f64; n_v];
for lm in &lmats {
let vi = &lm.vertex_indices;
for li in 0..3 {
let gi = vi[li];
grhs[gi] += lm.f_vol[li];
for lj in 0..3 {
let gj = vi[lj];
gmat[[gi, gj]] += lm.a_kk[[li, lj]];
}
}
}
apply_dirichlet(&mut gmat, &mut grhs, &mesh, g);
let trace_values = lu_solve(&mut gmat, &mut grhs)?;
let mut volume_values = Vec::with_capacity(n_e);
for lm in &lmats {
let vi = &lm.vertex_indices;
volume_values.push(vec![
trace_values[vi[0]],
trace_values[vi[1]],
trace_values[vi[2]],
]);
}
Ok(HdgSolution {
trace_values,
volume_values,
mesh,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn uniform_mesh(n: usize) -> HdgMesh {
let mut verts = Vec::new();
let mut elems = Vec::new();
for j in 0..=n {
for i in 0..=n {
verts.push([i as f64 / n as f64, j as f64 / n as f64]);
}
}
for j in 0..n {
for i in 0..n {
let v00 = j * (n + 1) + i;
let v10 = j * (n + 1) + i + 1;
let v01 = (j + 1) * (n + 1) + i;
let v11 = (j + 1) * (n + 1) + i + 1;
elems.push([v00, v10, v11]);
elems.push([v00, v11, v01]);
}
}
HdgMesh::new(verts, elems)
}
#[test]
fn test_hdg_poisson_constant_solution() {
let mesh = uniform_mesh(3);
let sol = solve_hdg(mesh, &|_, _| 0.0, &|_, _| 1.0, HdgConfig::default()).unwrap();
let err = sol.l2_error(&|_, _| 1.0);
assert!(err < 1e-10, "L2 err for u=1: {err}");
}
#[test]
fn test_hdg_poisson_linear_solution() {
let mesh = uniform_mesh(3);
let sol = solve_hdg(mesh, &|_, _| 0.0, &|x, y| x + y, HdgConfig::default()).unwrap();
let err = sol.l2_error(&|x, y| x + y);
assert!(err < 1e-8, "L2 err for u=x+y: {err}");
}
#[test]
fn test_hdg_l2_error_decay() {
use std::f64::consts::PI;
let f = move |x: f64, y: f64| 2.0 * PI * PI * (PI * x).sin() * (PI * y).sin();
let g = |_: f64, _: f64| 0.0_f64;
let u = |x: f64, y: f64| (PI * x).sin() * (PI * y).sin();
let ec = solve_hdg(uniform_mesh(3), &f, &g, HdgConfig::default())
.unwrap()
.l2_error(&u);
let ef = solve_hdg(uniform_mesh(6), &f, &g, HdgConfig::default())
.unwrap()
.l2_error(&u);
assert!(ef < ec, "Error should decay: coarse={ec}, fine={ef}");
}
}