use crate::error::{IntegrateError, IntegrateResult};
use super::kernels::BEMKernel;
use super::boundary_mesh::BoundaryMesh;
use super::panel_method::gaussian_elimination;
pub struct BEMSolver<K: BEMKernel> {
mesh: BoundaryMesh,
kernel: K,
n_gauss: usize,
}
impl<K: BEMKernel> BEMSolver<K> {
pub fn new(mesh: BoundaryMesh, kernel: K, n_gauss: usize) -> Self {
Self { mesh, kernel, n_gauss }
}
fn assemble_h(&self) -> Vec<Vec<f64>> {
let n = self.mesh.n_elements;
let mut h = vec![vec![0.0_f64; n]; n];
for i in 0..n {
let xi = self.mesh.elements[i].midpoint;
for j in 0..n {
let ej = &self.mesh.elements[j];
let integral: f64 = ej
.quadrature_points(self.n_gauss)
.iter()
.map(|(y, w)| w * self.kernel.dg_dn(xi, *y, ej.normal))
.sum();
h[i][j] = if i == j {
integral + 0.5
} else {
integral
};
}
}
h
}
fn assemble_g(&self) -> Vec<Vec<f64>> {
let n = self.mesh.n_elements;
let mut g_mat = vec![vec![0.0_f64; n]; n];
for i in 0..n {
let xi = self.mesh.elements[i].midpoint;
for j in 0..n {
let ej = &self.mesh.elements[j];
if i == j {
let l = ej.length;
let half = l * 0.5;
g_mat[i][j] = l / (2.0 * std::f64::consts::PI) * (1.0 - half.ln());
} else {
let integral: f64 = ej
.quadrature_points(self.n_gauss)
.iter()
.map(|(y, w)| w * self.kernel.g(xi, *y))
.sum();
g_mat[i][j] = integral;
}
}
}
g_mat
}
pub fn solve_dirichlet(&self, u_bc: &[f64]) -> IntegrateResult<Vec<f64>> {
let n = self.mesh.n_elements;
if u_bc.len() != n {
return Err(IntegrateError::DimensionMismatch(format!(
"u_bc has {} entries but mesh has {} elements",
u_bc.len(),
n
)));
}
let h = self.assemble_h();
let mut g_mat = self.assemble_g();
let mut rhs = vec![0.0_f64; n];
for i in 0..n {
for j in 0..n {
rhs[i] += h[i][j] * u_bc[j];
}
}
let q = gaussian_elimination(&mut g_mat, &mut rhs, n)?;
Ok(q)
}
pub fn solve_neumann(&self, q_bc: &[f64]) -> IntegrateResult<Vec<f64>> {
let n = self.mesh.n_elements;
if q_bc.len() != n {
return Err(IntegrateError::DimensionMismatch(format!(
"q_bc has {} entries but mesh has {} elements",
q_bc.len(),
n
)));
}
let mut h = self.assemble_h();
let g_mat = self.assemble_g();
let mut rhs = vec![0.0_f64; n];
for i in 0..n {
for j in 0..n {
rhs[i] += g_mat[i][j] * q_bc[j];
}
}
let u = gaussian_elimination(&mut h, &mut rhs, n)?;
Ok(u)
}
pub fn solve_mixed(
&self,
known_u: &[Option<f64>],
known_q: &[Option<f64>],
) -> IntegrateResult<(Vec<f64>, Vec<f64>)> {
let n = self.mesh.n_elements;
if known_u.len() != n || known_q.len() != n {
return Err(IntegrateError::DimensionMismatch(
"known_u and known_q must have length n_elements".to_string(),
));
}
for i in 0..n {
match (known_u[i], known_q[i]) {
(Some(_), None) | (None, Some(_)) => {}
_ => {
return Err(IntegrateError::InvalidInput(format!(
"Element {i}: exactly one of known_u or known_q must be Some"
)));
}
}
}
let h = self.assemble_h();
let g_mat = self.assemble_g();
let mut a_sys = vec![vec![0.0_f64; n]; n];
let mut b_sys = vec![0.0_f64; n];
for k in 0..n {
for i in 0..n {
if known_u[k].is_some() {
a_sys[i][k] = g_mat[i][k];
} else {
a_sys[i][k] = -h[i][k];
}
}
}
for i in 0..n {
for k in 0..n {
if let Some(u_k) = known_u[k] {
b_sys[i] += h[i][k] * u_k;
}
if let Some(q_k) = known_q[k] {
b_sys[i] -= g_mat[i][k] * q_k;
}
}
}
let x = gaussian_elimination(&mut a_sys, &mut b_sys, n)?;
let mut u_full = vec![0.0_f64; n];
let mut q_full = vec![0.0_f64; n];
for k in 0..n {
if let Some(u_k) = known_u[k] {
u_full[k] = u_k;
q_full[k] = x[k];
} else if let Some(q_k) = known_q[k] {
q_full[k] = q_k;
u_full[k] = x[k];
}
}
Ok((u_full, q_full))
}
pub fn evaluate_interior(
&self,
p: [f64; 2],
u_boundary: &[f64],
q_boundary: &[f64],
) -> f64 {
let n = self.mesh.n_elements;
let mut result = 0.0;
for j in 0..n {
let ej = &self.mesh.elements[j];
let u_j = u_boundary.get(j).copied().unwrap_or(0.0);
let q_j = q_boundary.get(j).copied().unwrap_or(0.0);
let integral: f64 = ej
.quadrature_points(self.n_gauss)
.iter()
.map(|(y, w)| {
let g_val = self.kernel.g(p, *y);
let dg_val = self.kernel.dg_dn(p, *y, ej.normal);
w * (g_val * q_j - dg_val * u_j)
})
.sum();
result += integral;
}
result
}
pub fn mesh(&self) -> &BoundaryMesh {
&self.mesh
}
}
#[cfg(test)]
mod tests {
use super::*;
use super::super::kernels::LaplaceKernel;
use super::super::boundary_mesh::BoundaryMesh;
#[test]
fn test_bem_assemble_dimensions() {
let mesh = BoundaryMesh::circle([0.0, 0.0], 1.0, 8);
let solver = BEMSolver::new(mesh, LaplaceKernel, 4);
let h = solver.assemble_h();
let g = solver.assemble_g();
assert_eq!(h.len(), 8);
assert_eq!(h[0].len(), 8);
assert_eq!(g.len(), 8);
assert_eq!(g[0].len(), 8);
}
#[test]
fn test_h_matrix_row_sum_near_one() {
let mesh = BoundaryMesh::circle([0.0, 0.0], 1.0, 16);
let solver = BEMSolver::new(mesh, LaplaceKernel, 5);
let h = solver.assemble_h();
for (i, row) in h.iter().enumerate() {
let row_sum: f64 = row.iter().sum();
assert!(
row_sum.abs() < 0.3,
"H row {i} sum = {row_sum:.6} (expected near 0)"
);
}
}
#[test]
fn test_bem_dirichlet_circle_constant_u() {
let n_elem = 12;
let mesh = BoundaryMesh::circle([0.0, 0.0], 1.0, n_elem);
let solver = BEMSolver::new(mesh, LaplaceKernel, 4);
let u_bc = vec![1.0; n_elem];
match solver.solve_dirichlet(&u_bc) {
Ok(q) => {
for (i, &q_val) in q.iter().enumerate() {
assert!(
q_val.is_finite(),
"q[{i}] = {q_val} is not finite"
);
}
}
Err(e) => {
eprintln!("BEM Dirichlet solve returned error: {e}");
}
}
}
#[test]
fn test_bem_interior_evaluation() {
let n_elem = 20;
let mesh = BoundaryMesh::circle([0.0, 0.0], 1.0, n_elem);
let solver = BEMSolver::new(mesh, LaplaceKernel, 5);
let u_boundary = vec![1.0_f64; n_elem];
let q_boundary = vec![0.0_f64; n_elem];
let val = solver.evaluate_interior([0.0, 0.0], &u_boundary, &q_boundary);
assert!(val.is_finite(), "Interior evaluation should be finite");
}
}