use std::f64::consts::PI;
pub(crate) fn gauss_legendre_1d(n: usize) -> (Vec<f64>, Vec<f64>) {
match n {
1 => (vec![0.0], vec![2.0]),
2 => (
vec![-0.577_350_269_189_625_8, 0.577_350_269_189_625_8],
vec![1.0, 1.0],
),
3 => (
vec![-0.774_596_669_241_483_4, 0.0, 0.774_596_669_241_483_4],
vec![
0.555_555_555_555_555_6,
0.888_888_888_888_888_9,
0.555_555_555_555_555_6,
],
),
4 => (
vec![
-0.861_136_311_594_052_6,
-0.339_981_043_584_856_3,
0.339_981_043_584_856_3,
0.861_136_311_594_052_6,
],
vec![
0.347_854_845_137_453_8,
0.652_145_154_862_546_1,
0.652_145_154_862_546_1,
0.347_854_845_137_453_8,
],
),
5 => (
vec![
-0.906_179_845_938_664_0,
-0.538_469_310_105_683_1,
0.0,
0.538_469_310_105_683_1,
0.906_179_845_938_664_0,
],
vec![
0.236_926_885_056_189_1,
0.478_628_670_499_366_5,
0.568_888_888_888_888_9,
0.478_628_670_499_366_5,
0.236_926_885_056_189_1,
],
),
_ => {
gauss_legendre_1d(5)
}
}
}
#[derive(Debug, Clone)]
pub struct BoundaryElement {
pub nodes: [[f64; 2]; 2],
pub normal: [f64; 2],
pub length: f64,
pub midpoint: [f64; 2],
}
impl BoundaryElement {
pub fn new(p1: [f64; 2], p2: [f64; 2]) -> Self {
let tx = p2[0] - p1[0];
let ty = p2[1] - p1[1];
let length = (tx * tx + ty * ty).sqrt();
let (nx, ny) = if length > 1e-30 {
(ty / length, -tx / length)
} else {
(0.0, 1.0)
};
let midpoint = [(p1[0] + p2[0]) * 0.5, (p1[1] + p2[1]) * 0.5];
Self {
nodes: [p1, p2],
normal: [nx, ny],
length,
midpoint,
}
}
pub fn quadrature_points(&self, n_points: usize) -> Vec<([f64; 2], f64)> {
let (xi, w) = gauss_legendre_1d(n_points);
let p1 = self.nodes[0];
let p2 = self.nodes[1];
let half = self.length * 0.5;
xi.iter()
.zip(w.iter())
.map(|(&xi_k, &w_k)| {
let t = (1.0 + xi_k) * 0.5; let pt = [
p1[0] + t * (p2[0] - p1[0]),
p1[1] + t * (p2[1] - p1[1]),
];
(pt, w_k * half)
})
.collect()
}
pub fn point_at(&self, t: f64) -> [f64; 2] {
let p1 = self.nodes[0];
let p2 = self.nodes[1];
[p1[0] + t * (p2[0] - p1[0]), p1[1] + t * (p2[1] - p1[1])]
}
}
#[derive(Debug, Clone)]
pub struct BoundaryMesh {
pub elements: Vec<BoundaryElement>,
pub n_elements: usize,
}
impl BoundaryMesh {
pub fn new() -> Self {
Self {
elements: Vec::new(),
n_elements: 0,
}
}
pub fn add_element(&mut self, elem: BoundaryElement) {
self.elements.push(elem);
self.n_elements += 1;
}
pub fn circle(center: [f64; 2], radius: f64, n_panels: usize) -> Self {
let mut mesh = Self::new();
for i in 0..n_panels {
let theta1 = 2.0 * PI * i as f64 / n_panels as f64;
let theta2 = 2.0 * PI * (i + 1) as f64 / n_panels as f64;
let p1 = [
center[0] + radius * theta1.cos(),
center[1] + radius * theta1.sin(),
];
let p2 = [
center[0] + radius * theta2.cos(),
center[1] + radius * theta2.sin(),
];
let mut elem = BoundaryElement::new(p1, p2);
let mid_theta = (theta1 + theta2) * 0.5;
elem.normal = [mid_theta.cos(), mid_theta.sin()];
mesh.add_element(elem);
}
mesh
}
pub fn rectangle(
x_min: f64,
x_max: f64,
y_min: f64,
y_max: f64,
n_panels_per_side: usize,
) -> Self {
let mut mesh = Self::new();
let n = n_panels_per_side;
for i in 0..n {
let x1 = x_min + (x_max - x_min) * i as f64 / n as f64;
let x2 = x_min + (x_max - x_min) * (i + 1) as f64 / n as f64;
let mut elem = BoundaryElement::new([x1, y_min], [x2, y_min]);
elem.normal = [0.0, -1.0];
mesh.add_element(elem);
}
for i in 0..n {
let y1 = y_min + (y_max - y_min) * i as f64 / n as f64;
let y2 = y_min + (y_max - y_min) * (i + 1) as f64 / n as f64;
let mut elem = BoundaryElement::new([x_max, y1], [x_max, y2]);
elem.normal = [1.0, 0.0];
mesh.add_element(elem);
}
for i in 0..n {
let x1 = x_max - (x_max - x_min) * i as f64 / n as f64;
let x2 = x_max - (x_max - x_min) * (i + 1) as f64 / n as f64;
let mut elem = BoundaryElement::new([x1, y_max], [x2, y_max]);
elem.normal = [0.0, 1.0];
mesh.add_element(elem);
}
for i in 0..n {
let y1 = y_max - (y_max - y_min) * i as f64 / n as f64;
let y2 = y_max - (y_max - y_min) * (i + 1) as f64 / n as f64;
let mut elem = BoundaryElement::new([x_min, y1], [x_min, y2]);
elem.normal = [-1.0, 0.0];
mesh.add_element(elem);
}
mesh
}
pub fn from_polygon(vertices: &[[f64; 2]]) -> Self {
let mut mesh = Self::new();
let n = vertices.len();
for i in 0..n {
let p1 = vertices[i];
let p2 = vertices[(i + 1) % n];
mesh.add_element(BoundaryElement::new(p1, p2));
}
mesh
}
pub fn total_length(&self) -> f64 {
self.elements.iter().map(|e| e.length).sum()
}
pub fn collocation_points(&self) -> Vec<[f64; 2]> {
self.elements.iter().map(|e| e.midpoint).collect()
}
}
impl Default for BoundaryMesh {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_element_normal_circle() {
let mesh = BoundaryMesh::circle([0.0, 0.0], 1.0, 8);
for elem in &mesh.elements {
let n = elem.normal;
let len = (n[0] * n[0] + n[1] * n[1]).sqrt();
assert!((len - 1.0).abs() < 1e-10, "Normal should be unit length");
let m = elem.midpoint;
let dot = m[0] * n[0] + m[1] * n[1];
assert!(dot > 0.0, "Normal should point outward from center");
}
}
#[test]
fn test_circle_total_length() {
let r = 1.5;
let n = 64;
let mesh = BoundaryMesh::circle([0.0, 0.0], r, n);
let total = mesh.total_length();
let exact = 2.0 * PI * r;
assert!(
(total - exact).abs() / exact < 0.002,
"Circle perimeter: got {total}, expected {exact}"
);
}
#[test]
fn test_rectangle_mesh_count() {
let mesh = BoundaryMesh::rectangle(0.0, 1.0, 0.0, 1.0, 4);
assert_eq!(mesh.n_elements, 16);
}
#[test]
fn test_quadrature_points_count() {
let elem = BoundaryElement::new([0.0, 0.0], [1.0, 0.0]);
let pts = elem.quadrature_points(4);
assert_eq!(pts.len(), 4);
}
#[test]
fn test_quadrature_weights_sum() {
let elem = BoundaryElement::new([0.0, 0.0], [2.0, 0.0]);
let pts = elem.quadrature_points(5);
let weight_sum: f64 = pts.iter().map(|(_, w)| w).sum();
assert!(
(weight_sum - elem.length).abs() < 1e-12,
"Weight sum {weight_sum} != length {}", elem.length
);
}
}