use std::f64::consts::PI;
#[inline]
fn dist3(a: &[f64; 3], b: &[f64; 3]) -> f64 {
let dx = a[0] - b[0];
let dy = a[1] - b[1];
let dz = a[2] - b[2];
(dx * dx + dy * dy + dz * dz).sqrt()
}
#[inline]
fn dot3(a: &[f64; 3], b: &[f64; 3]) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
#[inline]
fn cross3(a: &[f64; 3], b: &[f64; 3]) -> [f64; 3] {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
}
#[inline]
fn norm3(v: &[f64; 3]) -> f64 {
(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt()
}
#[inline]
fn normalize3(v: &mut [f64; 3]) -> f64 {
let len = norm3(v);
if len > f64::EPSILON {
v[0] /= len;
v[1] /= len;
v[2] /= len;
}
len
}
#[inline]
fn add3(a: &[f64; 3], b: &[f64; 3]) -> [f64; 3] {
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
#[inline]
fn sub3(a: &[f64; 3], b: &[f64; 3]) -> [f64; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
#[inline]
fn scale3(s: f64, v: &[f64; 3]) -> [f64; 3] {
[s * v[0], s * v[1], s * v[2]]
}
pub fn kelvin_laplace_3d(x: &[f64; 3], y: &[f64; 3]) -> f64 {
let r = dist3(x, y).max(f64::EPSILON);
1.0 / (4.0 * PI * r)
}
pub fn kelvin_laplace_3d_grad(x: &[f64; 3], y: &[f64; 3], normal: &[f64; 3]) -> f64 {
let rv = sub3(x, y);
let r = norm3(&rv).max(f64::EPSILON);
let r3 = r * r * r;
-dot3(&rv, normal) / (4.0 * PI * r3)
}
pub fn kelvin_laplace_2d(x: &[f64; 3], y: &[f64; 3]) -> f64 {
let r = dist3(x, y).max(f64::EPSILON);
-(r.ln()) / (2.0 * PI)
}
pub fn kelvin_displacement_3d(
x: &[f64; 3],
y: &[f64; 3],
i: usize,
j: usize,
shear_modulus: f64,
nu: f64,
) -> f64 {
let rv = sub3(x, y);
let r = norm3(&rv).max(f64::EPSILON);
let delta = if i == j { 1.0 } else { 0.0 };
let c = 1.0 / (16.0 * PI * shear_modulus * (1.0 - nu) * r);
c * ((3.0 - 4.0 * nu) * delta + rv[i] * rv[j] / (r * r))
}
pub fn kelvin_traction_3d(
x: &[f64; 3],
y: &[f64; 3],
normal: &[f64; 3],
i: usize,
j: usize,
nu: f64,
) -> f64 {
let rv = sub3(x, y);
let r = norm3(&rv).max(f64::EPSILON);
let r2 = r * r;
let drdn = dot3(&rv, normal) / r;
let delta = if i == j { 1.0 } else { 0.0 };
let c = -1.0 / (8.0 * PI * (1.0 - nu) * r2);
let term1 = drdn * ((1.0 - 2.0 * nu) * delta + 3.0 * rv[i] * rv[j] / r2);
let term2 = (1.0 - 2.0 * nu) * (normal[i] * rv[j] - normal[j] * rv[i]) / r;
c * (term1 + term2)
}
pub fn helmholtz_3d(x: &[f64; 3], y: &[f64; 3], k: f64) -> (f64, f64) {
let r = dist3(x, y).max(f64::EPSILON);
let c = 1.0 / (4.0 * PI * r);
let phase = k * r;
(c * phase.cos(), c * phase.sin())
}
pub fn helmholtz_3d_grad(x: &[f64; 3], y: &[f64; 3], normal: &[f64; 3], k: f64) -> (f64, f64) {
let rv = sub3(x, y);
let r = norm3(&rv).max(f64::EPSILON);
let r2 = r * r;
let drdn = dot3(&rv, normal) / r;
let phase = k * r;
let cos_kr = phase.cos();
let sin_kr = phase.sin();
let c = drdn / (4.0 * PI * r2);
let real = c * ((k * r * (-sin_kr)) - cos_kr);
let imag = c * ((k * r * cos_kr) - sin_kr);
(real, imag)
}
pub fn half_space_laplace_3d(x: &[f64; 3], y: &[f64; 3]) -> f64 {
let y_img = [y[0], y[1], -y[2]]; kelvin_laplace_3d(x, y) + kelvin_laplace_3d(x, &y_img)
}
pub fn half_space_laplace_3d_grad(x: &[f64; 3], y: &[f64; 3], normal: &[f64; 3]) -> f64 {
let y_img = [y[0], y[1], -y[2]];
kelvin_laplace_3d_grad(x, y, normal) + kelvin_laplace_3d_grad(x, &y_img, normal)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ElementOrder {
Constant,
Linear,
Quadratic,
}
#[derive(Debug, Clone)]
pub struct BoundaryElement {
pub node_ids: Vec<usize>,
pub normal: [f64; 3],
pub area: f64,
pub order: ElementOrder,
}
#[derive(Debug, Clone)]
pub struct BoundaryMesh {
pub nodes: Vec<[f64; 3]>,
pub elements: Vec<BoundaryElement>,
}
impl BoundaryMesh {
pub fn new() -> Self {
Self {
nodes: Vec::new(),
elements: Vec::new(),
}
}
pub fn num_elements(&self) -> usize {
self.elements.len()
}
pub fn num_nodes(&self) -> usize {
self.nodes.len()
}
pub fn element_centroid(&self, elem_idx: usize) -> [f64; 3] {
let elem = &self.elements[elem_idx];
let n = elem.node_ids.len() as f64;
let mut c = [0.0; 3];
for &nid in &elem.node_ids {
let p = &self.nodes[nid];
c[0] += p[0];
c[1] += p[1];
c[2] += p[2];
}
c[0] /= n;
c[1] /= n;
c[2] /= n;
c
}
pub fn unit_square(nx: usize, ny: usize) -> Self {
let mut nodes = Vec::new();
for iy in 0..=ny {
for ix in 0..=nx {
let x = ix as f64 / nx as f64;
let y = iy as f64 / ny as f64;
nodes.push([x, y, 0.0]);
}
}
let cols = nx + 1;
let mut elements = Vec::new();
for iy in 0..ny {
for ix in 0..nx {
let n0 = iy * cols + ix;
let n1 = n0 + 1;
let n2 = n0 + cols;
let n3 = n2 + 1;
let area1 = triangle_area(&nodes[n0], &nodes[n1], &nodes[n3]);
elements.push(BoundaryElement {
node_ids: vec![n0, n1, n3],
normal: [0.0, 0.0, 1.0],
area: area1,
order: ElementOrder::Constant,
});
let area2 = triangle_area(&nodes[n0], &nodes[n3], &nodes[n2]);
elements.push(BoundaryElement {
node_ids: vec![n0, n3, n2],
normal: [0.0, 0.0, 1.0],
area: area2,
order: ElementOrder::Constant,
});
}
}
Self { nodes, elements }
}
pub fn sphere(r: f64, n_lat: usize, n_lon: usize) -> Self {
let mut nodes = Vec::new();
nodes.push([0.0, 0.0, r]);
for i in 1..n_lat {
let theta = PI * i as f64 / n_lat as f64;
let st = theta.sin();
let ct = theta.cos();
for j in 0..n_lon {
let phi = 2.0 * PI * j as f64 / n_lon as f64;
nodes.push([r * st * phi.cos(), r * st * phi.sin(), r * ct]);
}
}
nodes.push([0.0, 0.0, -r]);
let mut elements = Vec::new();
for j in 0..n_lon {
let j_next = (j + 1) % n_lon;
let nids = vec![0, 1 + j, 1 + j_next];
let area = triangle_area(&nodes[nids[0]], &nodes[nids[1]], &nodes[nids[2]]);
let normal = element_normal(&nodes[nids[0]], &nodes[nids[1]], &nodes[nids[2]]);
elements.push(BoundaryElement {
node_ids: nids,
normal,
area,
order: ElementOrder::Constant,
});
}
for i in 0..(n_lat.saturating_sub(2)) {
let row0 = 1 + i * n_lon;
let row1 = 1 + (i + 1) * n_lon;
for j in 0..n_lon {
let j_next = (j + 1) % n_lon;
let a = row0 + j;
let b = row0 + j_next;
let c = row1 + j_next;
let d = row1 + j;
let area1 = triangle_area(&nodes[a], &nodes[b], &nodes[c]);
let norm1 = element_normal(&nodes[a], &nodes[b], &nodes[c]);
elements.push(BoundaryElement {
node_ids: vec![a, b, c],
normal: norm1,
area: area1,
order: ElementOrder::Constant,
});
let area2 = triangle_area(&nodes[a], &nodes[c], &nodes[d]);
let norm2 = element_normal(&nodes[a], &nodes[c], &nodes[d]);
elements.push(BoundaryElement {
node_ids: vec![a, c, d],
normal: norm2,
area: area2,
order: ElementOrder::Constant,
});
}
}
let bot = nodes.len() - 1;
let last_row = 1 + (n_lat - 2) * n_lon;
for j in 0..n_lon {
let j_next = (j + 1) % n_lon;
let nids = vec![last_row + j, bot, last_row + j_next];
let area = triangle_area(&nodes[nids[0]], &nodes[nids[1]], &nodes[nids[2]]);
let normal = element_normal(&nodes[nids[0]], &nodes[nids[1]], &nodes[nids[2]]);
elements.push(BoundaryElement {
node_ids: nids,
normal,
area,
order: ElementOrder::Constant,
});
}
Self { nodes, elements }
}
}
impl Default for BoundaryMesh {
fn default() -> Self {
Self::new()
}
}
fn triangle_area(a: &[f64; 3], b: &[f64; 3], c: &[f64; 3]) -> f64 {
let ab = sub3(b, a);
let ac = sub3(c, a);
let n = cross3(&ab, &ac);
0.5 * norm3(&n)
}
fn element_normal(a: &[f64; 3], b: &[f64; 3], c: &[f64; 3]) -> [f64; 3] {
let ab = sub3(b, a);
let ac = sub3(c, a);
let mut n = cross3(&ab, &ac);
normalize3(&mut n);
n
}
#[derive(Debug, Clone)]
pub struct TellesTransform {
pub eta_bar: f64,
coeffs: [f64; 4],
}
impl TellesTransform {
pub fn new(eta_bar: f64) -> Self {
let eb = eta_bar;
let eb2 = eb * eb;
let q = (1.0 + eb2).sqrt();
let _gamma = (eb + q).cbrt() + (eb - q).cbrt() + eb;
let alpha = 0.25 * (1.0 - eb2);
Self {
eta_bar: eb,
coeffs: [alpha, eb, 1.0, 0.0],
}
}
pub fn map(&self, xi: f64) -> f64 {
let diff = xi - self.eta_bar;
let mapped = xi + self.coeffs[0] * diff * diff * diff.signum();
mapped.clamp(-1.0, 1.0)
}
pub fn jacobian(&self, xi: f64) -> f64 {
let diff = xi - self.eta_bar;
1.0 + 3.0 * self.coeffs[0] * diff.abs()
}
}
fn gauss_legendre(n: usize) -> Vec<(f64, f64)> {
match n {
1 => vec![(0.0, 2.0)],
2 => vec![(-1.0 / 3.0_f64.sqrt(), 1.0), (1.0 / 3.0_f64.sqrt(), 1.0)],
3 => vec![
(-(3.0 / 5.0_f64).sqrt(), 5.0 / 9.0),
(0.0, 8.0 / 9.0),
((3.0 / 5.0_f64).sqrt(), 5.0 / 9.0),
],
4 => {
let a = ((3.0 - 2.0 * (6.0 / 5.0_f64).sqrt()) / 7.0).sqrt();
let b = ((3.0 + 2.0 * (6.0 / 5.0_f64).sqrt()) / 7.0).sqrt();
let wa = (18.0 + 30.0_f64.sqrt()) / 36.0;
let wb = (18.0 - 30.0_f64.sqrt()) / 36.0;
vec![(-b, wb), (-a, wa), (a, wa), (b, wb)]
}
_ => {
let pts = [
(-0.906_179_845_938_664, 0.236_926_885_056_189_1),
(-0.538_469_310_105_683, 0.478_628_670_499_366_5),
(0.0, 0.568_888_888_888_889),
(0.538_469_310_105_683, 0.478_628_670_499_366_5),
(0.906_179_845_938_664, 0.236_926_885_056_189_1),
];
pts.to_vec()
}
}
}
#[derive(Debug, Clone)]
pub struct DenseMatrix {
pub rows: usize,
pub cols: usize,
pub data: Vec<f64>,
}
impl DenseMatrix {
pub fn zeros(rows: usize, cols: usize) -> Self {
Self {
rows,
cols,
data: vec![0.0; rows * cols],
}
}
pub fn get(&self, i: usize, j: usize) -> f64 {
self.data[i * self.cols + j]
}
pub fn set(&mut self, i: usize, j: usize, val: f64) {
self.data[i * self.cols + j] = val;
}
pub fn add(&mut self, i: usize, j: usize, val: f64) {
self.data[i * self.cols + j] += val;
}
pub fn matvec(&self, x: &[f64]) -> Vec<f64> {
assert_eq!(x.len(), self.cols);
let mut y = vec![0.0; self.rows];
for (i, y_i) in y.iter_mut().enumerate().take(self.rows) {
let mut s = 0.0;
for (j, &x_j) in x.iter().enumerate().take(self.cols) {
s += self.data[i * self.cols + j] * x_j;
}
*y_i = s;
}
y
}
pub fn transpose(&self) -> Self {
let mut t = Self::zeros(self.cols, self.rows);
for i in 0..self.rows {
for j in 0..self.cols {
t.set(j, i, self.get(i, j));
}
}
t
}
}
pub fn assemble_laplace_bem(mesh: &BoundaryMesh) -> (DenseMatrix, DenseMatrix) {
let n = mesh.num_elements();
let mut h_mat = DenseMatrix::zeros(n, n);
let mut g_mat = DenseMatrix::zeros(n, n);
let quad = gauss_legendre(4);
for i in 0..n {
let xi = mesh.element_centroid(i);
for j in 0..n {
if i == j {
let a = mesh.elements[j].area;
let equiv_r = (a / PI).sqrt();
g_mat.set(i, j, a / (2.0 * PI * equiv_r));
} else {
let elem_j = &mesh.elements[j];
let xj = mesh.element_centroid(j);
let nj = &elem_j.normal;
let g_val = kelvin_laplace_3d(&xi, &xj) * elem_j.area;
let h_val = kelvin_laplace_3d_grad(&xi, &xj, nj) * elem_j.area;
g_mat.set(i, j, g_val);
h_mat.set(i, j, h_val);
}
}
}
apply_rigid_body_motion(&mut h_mat);
let _ = quad;
(h_mat, g_mat)
}
fn apply_rigid_body_motion(h: &mut DenseMatrix) {
let n = h.rows;
for i in 0..n {
let mut off_sum = 0.0;
for j in 0..n {
if j != i {
off_sum += h.get(i, j);
}
}
h.set(i, i, -off_sum);
}
}
pub fn assemble_elastostatic_bem(
mesh: &BoundaryMesh,
shear_modulus: f64,
nu: f64,
) -> (DenseMatrix, DenseMatrix) {
let n = mesh.num_elements();
let dim = 3 * n;
let mut h_mat = DenseMatrix::zeros(dim, dim);
let mut g_mat = DenseMatrix::zeros(dim, dim);
for ie in 0..n {
let xi = mesh.element_centroid(ie);
for je in 0..n {
let xj = mesh.element_centroid(je);
let nj = &mesh.elements[je].normal.clone();
let aj = mesh.elements[je].area;
for di in 0..3_usize {
let row = ie * 3 + di;
for dj in 0..3_usize {
let col = je * 3 + dj;
if ie == je {
let equiv_r = (aj / PI).sqrt();
let delta = if di == dj { 1.0 } else { 0.0 };
let g_val = aj * delta / (16.0 * PI * shear_modulus * (1.0 - nu) * equiv_r);
g_mat.set(row, col, g_val);
} else {
let u_ij = kelvin_displacement_3d(&xi, &xj, di, dj, shear_modulus, nu);
let t_ij = kelvin_traction_3d(&xi, &xj, nj, di, dj, nu);
g_mat.set(row, col, u_ij * aj);
h_mat.set(row, col, t_ij * aj);
}
}
}
}
}
for i in 0..n {
for di in 0..3_usize {
let row = i * 3 + di;
let mut off_sum = 0.0;
for j in 0..n {
if j != i {
off_sum += h_mat.get(row, j * 3 + di);
}
}
h_mat.set(row, i * 3 + di, -off_sum);
}
}
(h_mat, g_mat)
}
pub fn assemble_helmholtz_bem(
mesh: &BoundaryMesh,
k: f64,
) -> (DenseMatrix, DenseMatrix, DenseMatrix, DenseMatrix) {
let n = mesh.num_elements();
let mut h_re = DenseMatrix::zeros(n, n);
let mut h_im = DenseMatrix::zeros(n, n);
let mut g_re = DenseMatrix::zeros(n, n);
let mut g_im = DenseMatrix::zeros(n, n);
for i in 0..n {
let xi = mesh.element_centroid(i);
for j in 0..n {
if i == j {
let a = mesh.elements[j].area;
let equiv_r = (a / PI).sqrt();
g_re.set(i, j, a / (2.0 * PI * equiv_r));
} else {
let xj = mesh.element_centroid(j);
let nj = &mesh.elements[j].normal;
let aj = mesh.elements[j].area;
let (gr, gi) = helmholtz_3d(&xi, &xj, k);
let (hr, hi) = helmholtz_3d_grad(&xi, &xj, nj, k);
g_re.set(i, j, gr * aj);
g_im.set(i, j, gi * aj);
h_re.set(i, j, hr * aj);
h_im.set(i, j, hi * aj);
}
}
}
apply_rigid_body_motion(&mut h_re);
(h_re, h_im, g_re, g_im)
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum BemBcType {
Dirichlet,
Neumann,
}
#[derive(Debug, Clone)]
pub struct BemBc {
pub element_id: usize,
pub bc_type: BemBcType,
pub values: Vec<f64>,
}
pub fn solve_laplace_bem(h: &DenseMatrix, g: &DenseMatrix, bcs: &[BemBc]) -> (Vec<f64>, Vec<f64>) {
let n = h.rows;
assert_eq!(bcs.len(), n);
let mut a_mat = DenseMatrix::zeros(n, n);
let mut rhs = vec![0.0; n];
let mut u = vec![0.0; n];
let mut q = vec![0.0; n];
for bc in bcs {
match bc.bc_type {
BemBcType::Dirichlet => {
u[bc.element_id] = bc.values[0];
}
BemBcType::Neumann => {
q[bc.element_id] = bc.values[0];
}
}
}
for (i, rhs_i) in rhs.iter_mut().enumerate().take(n) {
for j in 0..n {
if bcs[j].bc_type == BemBcType::Neumann {
a_mat.set(i, j, h.get(i, j));
*rhs_i += g.get(i, j) * q[j];
} else {
a_mat.set(i, j, -g.get(i, j));
*rhs_i -= h.get(i, j) * u[j];
}
}
}
let x = gauss_solve(&a_mat, &rhs);
for (j, bc) in bcs.iter().enumerate() {
match bc.bc_type {
BemBcType::Neumann => u[j] = x[j],
BemBcType::Dirichlet => q[j] = x[j],
}
}
(u, q)
}
fn gauss_solve(a: &DenseMatrix, b: &[f64]) -> Vec<f64> {
let n = a.rows;
let mut aug = vec![0.0; n * (n + 1)];
for i in 0..n {
for j in 0..n {
aug[i * (n + 1) + j] = a.get(i, j);
}
aug[i * (n + 1) + n] = b[i];
}
for k in 0..n {
let mut max_val = aug[k * (n + 1) + k].abs();
let mut max_row = k;
for i in (k + 1)..n {
let val = aug[i * (n + 1) + k].abs();
if val > max_val {
max_val = val;
max_row = i;
}
}
if max_row != k {
for j in 0..=(n) {
aug.swap(k * (n + 1) + j, max_row * (n + 1) + j);
}
}
let pivot = aug[k * (n + 1) + k];
if pivot.abs() < 1e-15 {
continue; }
for i in (k + 1)..n {
let factor = aug[i * (n + 1) + k] / pivot;
for j in k..=(n) {
aug[i * (n + 1) + j] -= factor * aug[k * (n + 1) + j];
}
}
}
let mut x = vec![0.0; n];
for i in (0..n).rev() {
let pivot = aug[i * (n + 1) + i];
if pivot.abs() < 1e-15 {
continue;
}
let mut s = aug[i * (n + 1) + n];
for j in (i + 1)..n {
s -= aug[i * (n + 1) + j] * x[j];
}
x[i] = s / pivot;
}
x
}
pub fn interior_potential(mesh: &BoundaryMesh, u: &[f64], q: &[f64], point: &[f64; 3]) -> f64 {
let n = mesh.num_elements();
let mut val = 0.0;
for j in 0..n {
let xj = mesh.element_centroid(j);
let nj = &mesh.elements[j].normal;
let aj = mesh.elements[j].area;
let g_val = kelvin_laplace_3d(point, &xj);
let h_val = kelvin_laplace_3d_grad(point, &xj, nj);
val += (g_val * q[j] - h_val * u[j]) * aj;
}
val
}
pub fn interior_flux(mesh: &BoundaryMesh, u: &[f64], q: &[f64], point: &[f64; 3]) -> [f64; 3] {
let _n = mesh.num_elements();
let mut grad = [0.0; 3];
let eps = 1e-6;
for (k, grad_k) in grad.iter_mut().enumerate() {
let mut pp = *point;
let mut pm = *point;
pp[k] += eps;
pm[k] -= eps;
let fp = interior_potential(mesh, u, q, &pp);
let fm = interior_potential(mesh, u, q, &pm);
*grad_k = (fp - fm) / (2.0 * eps);
}
grad
}
#[derive(Debug, Clone)]
pub struct CrackElement {
pub node_ids: Vec<usize>,
pub normal: [f64; 3],
pub area: f64,
}
#[derive(Debug, Clone)]
pub struct DualBem {
pub boundary: BoundaryMesh,
pub crack_elements: Vec<CrackElement>,
pub shear_modulus: f64,
pub nu: f64,
}
impl DualBem {
pub fn new(
boundary: BoundaryMesh,
crack_elements: Vec<CrackElement>,
shear_modulus: f64,
nu: f64,
) -> Self {
Self {
boundary,
crack_elements,
shear_modulus,
nu,
}
}
pub fn compute_sif(&self, cod: &[f64; 3], crack_length: f64) -> [f64; 3] {
let r = crack_length.max(f64::EPSILON);
let factor = self.shear_modulus / (1.0 - self.nu) * (2.0 * PI / r).sqrt();
[factor * cod[0], factor * cod[1], factor * cod[2]]
}
pub fn total_dofs(&self) -> usize {
3 * (self.boundary.num_elements() + self.crack_elements.len())
}
pub fn assemble(&self) -> (DenseMatrix, Vec<f64>) {
let nb = self.boundary.num_elements();
let nc = self.crack_elements.len();
let n = nb + nc;
let dim = 3 * n;
let mat = DenseMatrix::zeros(dim, dim);
let rhs = vec![0.0; dim];
(mat, rhs)
}
}
#[derive(Debug, Clone)]
pub struct BemFemCoupling {
pub interface_nodes: Vec<usize>,
pub bem_mesh: BoundaryMesh,
pub condensed_stiffness: Option<DenseMatrix>,
}
impl BemFemCoupling {
pub fn new(interface_nodes: Vec<usize>, bem_mesh: BoundaryMesh) -> Self {
Self {
interface_nodes,
bem_mesh,
condensed_stiffness: None,
}
}
pub fn condense(&mut self) {
let (h, g) = assemble_laplace_bem(&self.bem_mesh);
let n = h.rows;
let mut g_inv = DenseMatrix::zeros(n, n);
for j in 0..n {
let mut e = vec![0.0; n];
e[j] = 1.0;
let col = gauss_solve(&g, &e);
for (i, &col_i) in col.iter().enumerate().take(n) {
g_inv.set(i, j, col_i);
}
}
let mut k = DenseMatrix::zeros(n, n);
for i in 0..n {
for j in 0..n {
let mut s = 0.0;
for m in 0..n {
s += h.get(i, m) * g_inv.get(m, j);
}
k.set(i, j, s);
}
}
self.condensed_stiffness = Some(k);
}
pub fn num_interface_dofs(&self) -> usize {
self.interface_nodes.len()
}
}
#[derive(Debug, Clone)]
pub struct Aabb {
pub min: [f64; 3],
pub max: [f64; 3],
}
impl Aabb {
pub fn new(min: [f64; 3], max: [f64; 3]) -> Self {
Self { min, max }
}
pub fn center(&self) -> [f64; 3] {
[
0.5 * (self.min[0] + self.max[0]),
0.5 * (self.min[1] + self.max[1]),
0.5 * (self.min[2] + self.max[2]),
]
}
pub fn diameter(&self) -> f64 {
dist3(&self.min, &self.max)
}
pub fn contains(&self, p: &[f64; 3]) -> bool {
p[0] >= self.min[0]
&& p[0] <= self.max[0]
&& p[1] >= self.min[1]
&& p[1] <= self.max[1]
&& p[2] >= self.min[2]
&& p[2] <= self.max[2]
}
}
#[derive(Debug, Clone)]
pub struct OctreeNode {
pub bbox: Aabb,
pub sources: Vec<usize>,
pub children: Vec<OctreeNode>,
pub total_charge: f64,
pub charge_center: [f64; 3],
}
impl OctreeNode {
pub fn leaf(bbox: Aabb) -> Self {
Self {
bbox,
sources: Vec::new(),
children: Vec::new(),
total_charge: 0.0,
charge_center: [0.0; 3],
}
}
pub fn is_leaf(&self) -> bool {
self.children.is_empty()
}
pub fn compute_moments(&mut self, positions: &[[f64; 3]], charges: &[f64]) {
if self.is_leaf() {
self.total_charge = 0.0;
self.charge_center = [0.0; 3];
for &idx in &self.sources {
self.total_charge += charges[idx];
self.charge_center[0] += charges[idx] * positions[idx][0];
self.charge_center[1] += charges[idx] * positions[idx][1];
self.charge_center[2] += charges[idx] * positions[idx][2];
}
if self.total_charge.abs() > f64::EPSILON {
self.charge_center[0] /= self.total_charge;
self.charge_center[1] /= self.total_charge;
self.charge_center[2] /= self.total_charge;
}
} else {
self.total_charge = 0.0;
self.charge_center = [0.0; 3];
for child in &mut self.children {
child.compute_moments(positions, charges);
let q = child.total_charge;
self.total_charge += q;
self.charge_center[0] += q * child.charge_center[0];
self.charge_center[1] += q * child.charge_center[1];
self.charge_center[2] += q * child.charge_center[2];
}
if self.total_charge.abs() > f64::EPSILON {
self.charge_center[0] /= self.total_charge;
self.charge_center[1] /= self.total_charge;
self.charge_center[2] /= self.total_charge;
}
}
}
}
pub fn build_octree(positions: &[[f64; 3]], charges: &[f64], max_leaf_size: usize) -> OctreeNode {
let mut bmin = [f64::MAX; 3];
let mut bmax = [f64::MIN; 3];
for p in positions {
for (k, (bmin_k, bmax_k)) in bmin.iter_mut().zip(bmax.iter_mut()).enumerate() {
*bmin_k = bmin_k.min(p[k]);
*bmax_k = bmax_k.max(p[k]);
}
}
for (bmin_k, bmax_k) in bmin.iter_mut().zip(bmax.iter_mut()) {
*bmin_k -= 1e-10;
*bmax_k += 1e-10;
}
let indices: Vec<usize> = (0..positions.len()).collect();
let mut root = OctreeNode::leaf(Aabb::new(bmin, bmax));
root.sources = indices;
subdivide_octree(&mut root, positions, max_leaf_size);
root.compute_moments(positions, charges);
root
}
fn subdivide_octree(node: &mut OctreeNode, positions: &[[f64; 3]], max_leaf_size: usize) {
if node.sources.len() <= max_leaf_size {
return;
}
let center = node.bbox.center();
let bmin = node.bbox.min;
let bmax = node.bbox.max;
let mut child_boxes = Vec::with_capacity(8);
for iz in 0..2_usize {
for iy in 0..2_usize {
for ix in 0..2_usize {
let cmin = [
if ix == 0 { bmin[0] } else { center[0] },
if iy == 0 { bmin[1] } else { center[1] },
if iz == 0 { bmin[2] } else { center[2] },
];
let cmax = [
if ix == 0 { center[0] } else { bmax[0] },
if iy == 0 { center[1] } else { bmax[1] },
if iz == 0 { center[2] } else { bmax[2] },
];
child_boxes.push(Aabb::new(cmin, cmax));
}
}
}
let mut children: Vec<OctreeNode> = child_boxes.into_iter().map(OctreeNode::leaf).collect();
for &idx in &node.sources {
let p = &positions[idx];
for child in &mut children {
if child.bbox.contains(p) {
child.sources.push(idx);
break;
}
}
}
children.retain(|c| !c.sources.is_empty());
for child in &mut children {
subdivide_octree(child, positions, max_leaf_size);
}
node.children = children;
node.sources.clear();
}
pub fn barnes_hut_potential(node: &OctreeNode, target: &[f64; 3], theta: f64) -> f64 {
if node.is_leaf() {
let r = dist3(target, &node.charge_center).max(f64::EPSILON);
return node.total_charge / (4.0 * PI * r);
}
let r = dist3(target, &node.charge_center).max(f64::EPSILON);
let d = node.bbox.diameter();
if d / r < theta {
node.total_charge / (4.0 * PI * r)
} else {
let mut pot = 0.0;
for child in &node.children {
pot += barnes_hut_potential(child, target, theta);
}
pot
}
}
pub fn fast_multipole_eval(
positions: &[[f64; 3]],
charges: &[f64],
targets: &[[f64; 3]],
theta: f64,
) -> Vec<f64> {
let tree = build_octree(positions, charges, 8);
targets
.iter()
.map(|t| barnes_hut_potential(&tree, t, theta))
.collect()
}
#[derive(Debug, Clone)]
pub struct LaplaceBemProblem {
pub mesh: BoundaryMesh,
pub bcs: Vec<BemBc>,
}
impl LaplaceBemProblem {
pub fn new(mesh: BoundaryMesh, bcs: Vec<BemBc>) -> Self {
Self { mesh, bcs }
}
pub fn solve(&self) -> (Vec<f64>, Vec<f64>) {
let (h, g) = assemble_laplace_bem(&self.mesh);
solve_laplace_bem(&h, &g, &self.bcs)
}
}
#[derive(Debug, Clone)]
pub struct HelmholtzBemProblem {
pub mesh: BoundaryMesh,
pub k: f64,
pub incident: Vec<(f64, f64)>,
}
impl HelmholtzBemProblem {
pub fn new(mesh: BoundaryMesh, k: f64) -> Self {
let n = mesh.num_elements();
Self {
mesh,
k,
incident: vec![(0.0, 0.0); n],
}
}
pub fn set_plane_wave(&mut self, amplitude: f64, dir: &[f64; 3]) {
let n = self.mesh.num_elements();
for i in 0..n {
let c = self.mesh.element_centroid(i);
let phase = self.k * dot3(&c, dir);
self.incident[i] = (amplitude * phase.cos(), amplitude * phase.sin());
}
}
pub fn num_elements(&self) -> usize {
self.mesh.num_elements()
}
}
#[derive(Debug, Clone)]
pub struct ElastostaticBemProblem {
pub mesh: BoundaryMesh,
pub shear_modulus: f64,
pub nu: f64,
pub bcs: Vec<BemBc>,
}
impl ElastostaticBemProblem {
pub fn new(mesh: BoundaryMesh, shear_modulus: f64, nu: f64) -> Self {
Self {
mesh,
shear_modulus,
nu,
bcs: Vec::new(),
}
}
pub fn add_bc(&mut self, bc: BemBc) {
self.bcs.push(bc);
}
pub fn num_elements(&self) -> usize {
self.mesh.num_elements()
}
}
pub fn linear_shape_functions(xi: f64, eta: f64) -> [f64; 3] {
[1.0 - xi - eta, xi, eta]
}
pub fn quadratic_shape_functions(xi: f64, eta: f64) -> [f64; 6] {
let l1 = 1.0 - xi - eta;
let l2 = xi;
let l3 = eta;
[
l1 * (2.0 * l1 - 1.0), l2 * (2.0 * l2 - 1.0), l3 * (2.0 * l3 - 1.0), 4.0 * l1 * l2, 4.0 * l2 * l3, 4.0 * l3 * l1, ]
}
pub fn interpolate_triangle(nodes: &[[f64; 3]; 3], xi: f64, eta: f64) -> [f64; 3] {
let n = linear_shape_functions(xi, eta);
[
n[0] * nodes[0][0] + n[1] * nodes[1][0] + n[2] * nodes[2][0],
n[0] * nodes[0][1] + n[1] * nodes[1][1] + n[2] * nodes[2][1],
n[0] * nodes[0][2] + n[1] * nodes[1][2] + n[2] * nodes[2][2],
]
}
pub fn bem_residual(
_mesh: &BoundaryMesh,
h: &DenseMatrix,
g: &DenseMatrix,
u: &[f64],
q: &[f64],
) -> Vec<f64> {
let hu = h.matvec(u);
let gq = g.matvec(q);
let residual: Vec<f64> = hu
.iter()
.zip(gq.iter())
.map(|(h_i, g_i)| (h_i - g_i).abs())
.collect();
residual
}
pub fn bem_residual_norm(residual: &[f64]) -> f64 {
residual.iter().map(|r| r * r).sum::<f64>().sqrt()
}
pub fn adaptive_refine(mesh: &BoundaryMesh, residual: &[f64], threshold: f64) -> BoundaryMesh {
let mut new_mesh = BoundaryMesh::new();
new_mesh.nodes = mesh.nodes.clone();
for (idx, elem) in mesh.elements.iter().enumerate() {
if residual[idx] > threshold && elem.node_ids.len() == 3 {
let n0 = elem.node_ids[0];
let n1 = elem.node_ids[1];
let n2 = elem.node_ids[2];
let p0 = mesh.nodes[n0];
let p1 = mesh.nodes[n1];
let p2 = mesh.nodes[n2];
let m01 = scale3(0.5, &add3(&p0, &p1));
let m12 = scale3(0.5, &add3(&p1, &p2));
let m20 = scale3(0.5, &add3(&p2, &p0));
let nm01 = new_mesh.nodes.len();
new_mesh.nodes.push(m01);
let nm12 = new_mesh.nodes.len();
new_mesh.nodes.push(m12);
let nm20 = new_mesh.nodes.len();
new_mesh.nodes.push(m20);
let sub_tris = [
[n0, nm01, nm20],
[nm01, n1, nm12],
[nm12, n2, nm20],
[nm01, nm12, nm20],
];
for tri in &sub_tris {
let area = triangle_area(
&new_mesh.nodes[tri[0]],
&new_mesh.nodes[tri[1]],
&new_mesh.nodes[tri[2]],
);
let normal = element_normal(
&new_mesh.nodes[tri[0]],
&new_mesh.nodes[tri[1]],
&new_mesh.nodes[tri[2]],
);
new_mesh.elements.push(BoundaryElement {
node_ids: tri.to_vec(),
normal,
area,
order: ElementOrder::Constant,
});
}
} else {
new_mesh.elements.push(elem.clone());
}
}
new_mesh
}
fn triangle_quadrature(order: usize) -> Vec<(f64, f64, f64)> {
match order {
1 => vec![(1.0 / 3.0, 1.0 / 3.0, 0.5)],
3 => vec![
(1.0 / 6.0, 1.0 / 6.0, 1.0 / 6.0),
(2.0 / 3.0, 1.0 / 6.0, 1.0 / 6.0),
(1.0 / 6.0, 2.0 / 3.0, 1.0 / 6.0),
],
_ => {
vec![
(1.0 / 3.0, 1.0 / 3.0, -27.0 / 96.0),
(0.6, 0.2, 25.0 / 96.0),
(0.2, 0.6, 25.0 / 96.0),
(0.2, 0.2, 25.0 / 96.0),
]
}
}
}
pub fn integrate_over_triangle<F>(nodes: &[[f64; 3]; 3], f: F, quad_order: usize) -> f64
where
F: Fn(&[f64; 3]) -> f64,
{
let pts = triangle_quadrature(quad_order);
let area = triangle_area(&nodes[0], &nodes[1], &nodes[2]);
let mut result = 0.0;
for (xi, eta, w) in pts {
let p = interpolate_triangle(nodes, xi, eta);
result += w * f(&p);
}
result * 2.0 * area }
#[cfg(test)]
mod tests {
use super::*;
const TOL: f64 = 1e-6;
#[test]
fn test_dist3_zero() {
let a = [1.0, 2.0, 3.0];
assert!(dist3(&a, &a) < TOL);
}
#[test]
fn test_dist3_basic() {
let a = [0.0; 3];
let b = [3.0, 4.0, 0.0];
assert!((dist3(&a, &b) - 5.0).abs() < TOL);
}
#[test]
fn test_dot3() {
let a = [1.0, 0.0, 0.0];
let b = [0.0, 1.0, 0.0];
assert!(dot3(&a, &b).abs() < TOL);
}
#[test]
fn test_cross3() {
let a = [1.0, 0.0, 0.0];
let b = [0.0, 1.0, 0.0];
let c = cross3(&a, &b);
assert!((c[2] - 1.0).abs() < TOL);
}
#[test]
fn test_normalize3() {
let mut v = [3.0, 4.0, 0.0];
let len = normalize3(&mut v);
assert!((len - 5.0).abs() < TOL);
assert!((norm3(&v) - 1.0).abs() < TOL);
}
#[test]
fn test_kelvin_laplace_3d_symmetry() {
let x = [1.0, 0.0, 0.0];
let y = [0.0, 1.0, 0.0];
assert!((kelvin_laplace_3d(&x, &y) - kelvin_laplace_3d(&y, &x)).abs() < TOL);
}
#[test]
fn test_kelvin_laplace_3d_value() {
let x = [1.0, 0.0, 0.0];
let y = [0.0, 0.0, 0.0];
let expected = 1.0 / (4.0 * PI);
assert!((kelvin_laplace_3d(&x, &y) - expected).abs() < TOL);
}
#[test]
fn test_kelvin_laplace_2d() {
let x = [1.0, 0.0, 0.0];
let y = [0.0, 0.0, 0.0];
let expected = 0.0; assert!((kelvin_laplace_2d(&x, &y) - expected).abs() < TOL);
}
#[test]
fn test_helmholtz_3d_reduces_to_laplace() {
let x = [1.0, 0.0, 0.0];
let y = [0.0, 0.0, 0.0];
let (re, im) = helmholtz_3d(&x, &y, 0.0);
let laplace = kelvin_laplace_3d(&x, &y);
assert!((re - laplace).abs() < TOL);
assert!(im.abs() < TOL);
}
#[test]
fn test_kelvin_displacement_symmetry() {
let x = [2.0, 0.0, 0.0];
let y = [0.0, 0.0, 0.0];
let g = 1.0;
let nu = 0.3;
let u01 = kelvin_displacement_3d(&x, &y, 0, 1, g, nu);
let u10 = kelvin_displacement_3d(&x, &y, 1, 0, g, nu);
assert!((u01 - u10).abs() < TOL);
}
#[test]
fn test_half_space_greens_function() {
let x = [1.0, 0.0, 0.0];
let y = [0.0, 0.0, 0.0];
let hs = half_space_laplace_3d(&x, &y);
let fs = kelvin_laplace_3d(&x, &y);
assert!((hs - 2.0 * fs).abs() < TOL);
}
#[test]
fn test_triangle_area() {
let a = [0.0, 0.0, 0.0];
let b = [1.0, 0.0, 0.0];
let c = [0.0, 1.0, 0.0];
let area = triangle_area(&a, &b, &c);
assert!((area - 0.5).abs() < TOL);
}
#[test]
fn test_element_normal() {
let a = [0.0, 0.0, 0.0];
let b = [1.0, 0.0, 0.0];
let c = [0.0, 1.0, 0.0];
let n = element_normal(&a, &b, &c);
assert!((n[2] - 1.0).abs() < TOL);
}
#[test]
fn test_boundary_mesh_unit_square() {
let mesh = BoundaryMesh::unit_square(2, 2);
assert_eq!(mesh.num_elements(), 8); assert_eq!(mesh.num_nodes(), 9); }
#[test]
fn test_boundary_mesh_sphere() {
let mesh = BoundaryMesh::sphere(1.0, 4, 8);
assert!(mesh.num_elements() > 0);
assert!(mesh.num_nodes() > 0);
}
#[test]
fn test_dense_matrix_basics() {
let mut m = DenseMatrix::zeros(3, 3);
m.set(0, 0, 1.0);
m.set(1, 1, 2.0);
m.set(2, 2, 3.0);
assert!((m.get(1, 1) - 2.0).abs() < TOL);
let x = vec![1.0, 1.0, 1.0];
let y = m.matvec(&x);
assert!((y[0] - 1.0).abs() < TOL);
assert!((y[1] - 2.0).abs() < TOL);
}
#[test]
fn test_dense_matrix_transpose() {
let mut m = DenseMatrix::zeros(2, 3);
m.set(0, 1, 5.0);
let t = m.transpose();
assert_eq!(t.rows, 3);
assert_eq!(t.cols, 2);
assert!((t.get(1, 0) - 5.0).abs() < TOL);
}
#[test]
fn test_gauss_solve_identity() {
let mut a = DenseMatrix::zeros(3, 3);
a.set(0, 0, 1.0);
a.set(1, 1, 1.0);
a.set(2, 2, 1.0);
let b = vec![1.0, 2.0, 3.0];
let x = gauss_solve(&a, &b);
for (&xi, &bi) in x.iter().zip(b.iter()) {
assert!((xi - bi).abs() < TOL);
}
}
#[test]
fn test_gauss_solve_2x2() {
let mut a = DenseMatrix::zeros(2, 2);
a.set(0, 0, 2.0);
a.set(0, 1, 1.0);
a.set(1, 0, 1.0);
a.set(1, 1, 3.0);
let b = vec![5.0, 7.0];
let x = gauss_solve(&a, &b);
assert!((x[0] - 1.6).abs() < TOL);
assert!((x[1] - 1.8).abs() < TOL);
}
#[test]
fn test_telles_transform() {
let t = TellesTransform::new(0.0);
let mapped = t.map(0.0);
assert!(mapped.abs() < 1.0);
let jac = t.jacobian(0.5);
assert!(jac > 0.0);
}
#[test]
fn test_linear_shape_functions_sum() {
let n = linear_shape_functions(0.3, 0.2);
let sum: f64 = n.iter().sum();
assert!((sum - 1.0).abs() < TOL);
}
#[test]
fn test_quadratic_shape_functions_sum() {
let n = quadratic_shape_functions(0.3, 0.2);
let sum: f64 = n.iter().sum();
assert!((sum - 1.0).abs() < TOL);
}
#[test]
fn test_interpolate_triangle_centroid() {
let nodes = [[0.0, 0.0, 0.0], [3.0, 0.0, 0.0], [0.0, 3.0, 0.0]];
let c = interpolate_triangle(&nodes, 1.0 / 3.0, 1.0 / 3.0);
assert!((c[0] - 1.0).abs() < TOL);
assert!((c[1] - 1.0).abs() < TOL);
}
#[test]
fn test_assemble_laplace_bem_dimensions() {
let mesh = BoundaryMesh::unit_square(2, 2);
let (h, g) = assemble_laplace_bem(&mesh);
assert_eq!(h.rows, 8);
assert_eq!(g.cols, 8);
}
#[test]
fn test_rigid_body_motion() {
let mesh = BoundaryMesh::unit_square(2, 2);
let (h, _g) = assemble_laplace_bem(&mesh);
let ones = vec![1.0; h.cols];
let hu = h.matvec(&ones);
let norm: f64 = hu.iter().map(|v| v * v).sum::<f64>().sqrt();
assert!(
norm < 1e-10,
"Rigid body condition failed: norm = {:.6e}",
norm
);
}
#[test]
fn test_laplace_bem_solve_basic() {
let mesh = BoundaryMesh::unit_square(1, 1);
let n = mesh.num_elements();
let bcs: Vec<BemBc> = (0..n)
.map(|i| BemBc {
element_id: i,
bc_type: BemBcType::Dirichlet,
values: vec![1.0],
})
.collect();
let (h, g) = assemble_laplace_bem(&mesh);
let (u, _q) = solve_laplace_bem(&h, &g, &bcs);
for val in &u {
assert!((val - 1.0).abs() < TOL);
}
}
#[test]
fn test_assemble_elastostatic_bem_dimensions() {
let mesh = BoundaryMesh::unit_square(1, 1);
let (h, g) = assemble_elastostatic_bem(&mesh, 1.0, 0.3);
let n = mesh.num_elements();
assert_eq!(h.rows, 3 * n);
assert_eq!(g.cols, 3 * n);
}
#[test]
fn test_assemble_helmholtz_bem_dimensions() {
let mesh = BoundaryMesh::unit_square(1, 1);
let (hr, hi, gr, gi) = assemble_helmholtz_bem(&mesh, 1.0);
let n = mesh.num_elements();
assert_eq!(hr.rows, n);
assert_eq!(hi.cols, n);
assert_eq!(gr.rows, n);
assert_eq!(gi.cols, n);
}
#[test]
fn test_octree_build() {
let positions = vec![
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[1.0, 1.0, 0.0],
];
let charges = vec![1.0, 1.0, 1.0, 1.0];
let tree = build_octree(&positions, &charges, 2);
assert!((tree.total_charge - 4.0).abs() < TOL);
}
#[test]
fn test_barnes_hut_far_field() {
let positions = vec![[0.0, 0.0, 0.0]];
let charges = vec![4.0 * PI]; let tree = build_octree(&positions, &charges, 2);
let target = [1.0, 0.0, 0.0];
let pot = barnes_hut_potential(&tree, &target, 0.5);
assert!((pot - 1.0).abs() < 0.1);
}
#[test]
fn test_fast_multipole_eval() {
let positions = vec![[0.0, 0.0, 0.0]];
let charges = vec![4.0 * PI];
let targets = vec![[2.0, 0.0, 0.0], [0.0, 2.0, 0.0]];
let pots = fast_multipole_eval(&positions, &charges, &targets, 0.5);
assert_eq!(pots.len(), 2);
for p in &pots {
assert!((p - 0.5).abs() < 0.1);
}
}
#[test]
fn test_dual_bem_sif() {
let mesh = BoundaryMesh::new();
let crack = vec![CrackElement {
node_ids: vec![0, 1, 2],
normal: [0.0, 0.0, 1.0],
area: 1.0,
}];
let dbem = DualBem::new(mesh, crack, 1.0, 0.3);
let cod = [0.01, 0.0, 0.0];
let sif = dbem.compute_sif(&cod, 0.1);
assert!(sif[0] > 0.0);
}
#[test]
fn test_bem_fem_coupling_new() {
let mesh = BoundaryMesh::unit_square(1, 1);
let coupling = BemFemCoupling::new(vec![0, 1, 2], mesh);
assert_eq!(coupling.num_interface_dofs(), 3);
}
#[test]
fn test_bem_residual_zeros() {
let mesh = BoundaryMesh::unit_square(1, 1);
let n = mesh.num_elements();
let (h, g) = assemble_laplace_bem(&mesh);
let u = vec![0.0; n];
let q = vec![0.0; n];
let res = bem_residual(&mesh, &h, &g, &u, &q);
let norm = bem_residual_norm(&res);
assert!(norm < TOL);
}
#[test]
fn test_adaptive_refine() {
let mesh = BoundaryMesh::unit_square(1, 1);
let residual = vec![1.0, 0.01]; let refined = adaptive_refine(&mesh, &residual, 0.5);
assert_eq!(refined.num_elements(), 5);
}
#[test]
fn test_integrate_over_triangle() {
let nodes = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
let result = integrate_over_triangle(&nodes, |_p| 1.0, 3);
assert!((result - 0.5).abs() < 0.05);
}
#[test]
fn test_helmholtz_problem_plane_wave() {
let mesh = BoundaryMesh::unit_square(2, 2);
let mut prob = HelmholtzBemProblem::new(mesh, 1.0);
prob.set_plane_wave(1.0, &[1.0, 0.0, 0.0]);
assert_eq!(prob.num_elements(), 8);
let has_nonzero = prob
.incident
.iter()
.any(|(r, i)| r.abs() > TOL || i.abs() > TOL);
assert!(has_nonzero);
}
#[test]
fn test_gauss_legendre_weights_sum() {
for n in 1..=5 {
let pts = gauss_legendre(n);
let sum: f64 = pts.iter().map(|(_, w)| w).sum();
assert!((sum - 2.0).abs() < 0.01, "GL({n}) weights sum = {sum:.6}");
}
}
#[test]
fn test_aabb_contains() {
let bb = Aabb::new([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
assert!(bb.contains(&[0.5, 0.5, 0.5]));
assert!(!bb.contains(&[1.5, 0.5, 0.5]));
}
#[test]
fn test_aabb_diameter() {
let bb = Aabb::new([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
let d = bb.diameter();
assert!((d - 3.0_f64.sqrt()).abs() < TOL);
}
}