use crate::error::{IntegrateError, IntegrateResult};
use super::boundary_mesh::{BoundaryMesh, gauss_legendre_1d};
use std::f64::consts::PI;
#[derive(Debug, Clone)]
pub struct PanelMethodConfig {
pub n_gauss: usize,
pub free_stream: [f64; 2],
}
impl Default for PanelMethodConfig {
fn default() -> Self {
Self {
n_gauss: 5,
free_stream: [1.0, 0.0],
}
}
}
fn source_panel_g(x: [f64; 2], p1: [f64; 2], p2: [f64; 2], n_gauss: usize) -> f64 {
let (xi_nodes, weights) = gauss_legendre_1d(n_gauss);
let half_len = {
let dx = p2[0] - p1[0];
let dy = p2[1] - p1[1];
(dx * dx + dy * dy).sqrt() * 0.5
};
xi_nodes
.iter()
.zip(weights.iter())
.map(|(&xi, &w)| {
let t = (1.0 + xi) * 0.5;
let y = [
p1[0] + t * (p2[0] - p1[0]),
p1[1] + t * (p2[1] - p1[1]),
];
let r = ((x[0] - y[0]).powi(2) + (x[1] - y[1]).powi(2)).sqrt();
if r < 1e-14 {
0.0
} else {
-r.ln() / (2.0 * PI) * w * half_len
}
})
.sum()
}
fn doublet_panel_h(x: [f64; 2], p1: [f64; 2], p2: [f64; 2], normal: [f64; 2], n_gauss: usize) -> f64 {
let (xi_nodes, weights) = gauss_legendre_1d(n_gauss);
let dx_panel = p2[0] - p1[0];
let dy_panel = p2[1] - p1[1];
let len = (dx_panel * dx_panel + dy_panel * dy_panel).sqrt();
let half_len = len * 0.5;
xi_nodes
.iter()
.zip(weights.iter())
.map(|(&xi, &w)| {
let t = (1.0 + xi) * 0.5;
let y = [
p1[0] + t * dx_panel,
p1[1] + t * dy_panel,
];
let drx = x[0] - y[0];
let dry = x[1] - y[1];
let r2 = drx * drx + dry * dry;
if r2 < 1e-28 {
0.0
} else {
(drx * normal[0] + dry * normal[1]) / (2.0 * PI * r2) * w * half_len
}
})
.sum()
}
pub struct PanelMethod {
mesh: BoundaryMesh,
cfg: PanelMethodConfig,
source_strengths: Vec<f64>,
}
impl PanelMethod {
pub fn new(mesh: BoundaryMesh, cfg: PanelMethodConfig) -> Self {
Self {
mesh,
cfg,
source_strengths: Vec::new(),
}
}
fn assemble_influence_matrix(&self) -> Vec<Vec<f64>> {
let n = self.mesh.n_elements;
let mut a = 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 val = doublet_panel_h(
xi,
ej.nodes[0],
ej.nodes[1],
ej.normal,
self.cfg.n_gauss,
);
a[i][j] = if i == j { val - 0.5 } else { val };
}
}
a
}
pub fn solve(&mut self) -> IntegrateResult<()> {
let n = self.mesh.n_elements;
let mut a = self.assemble_influence_matrix();
let u_inf = self.cfg.free_stream;
let mut b: Vec<f64> = self
.mesh
.elements
.iter()
.map(|e| -(u_inf[0] * e.normal[0] + u_inf[1] * e.normal[1]))
.collect();
let sigma = gaussian_elimination(&mut a, &mut b, n)?;
self.source_strengths = sigma;
Ok(())
}
pub fn velocity_at(&self, p: [f64; 2]) -> [f64; 2] {
let mut vx = 0.0_f64;
let mut vy = 0.0_f64;
let (xi_nodes, weights) = gauss_legendre_1d(self.cfg.n_gauss);
for (j, elem) in self.mesh.elements.iter().enumerate() {
let sigma_j = self.source_strengths.get(j).copied().unwrap_or(0.0);
let half = elem.length * 0.5;
let p1 = elem.nodes[0];
let p2 = elem.nodes[1];
for (&xi, &w) in xi_nodes.iter().zip(weights.iter()) {
let t = (1.0 + xi) * 0.5;
let y = [p1[0] + t * (p2[0] - p1[0]), p1[1] + t * (p2[1] - p1[1])];
let dx = p[0] - y[0];
let dy = p[1] - y[1];
let r2 = dx * dx + dy * dy;
if r2 < 1e-28 {
continue;
}
let factor = sigma_j * w * half / (2.0 * PI * r2);
vx += factor * dx;
vy += factor * dy;
}
}
[vx + self.cfg.free_stream[0], vy + self.cfg.free_stream[1]]
}
pub fn surface_cp(&self) -> IntegrateResult<(Vec<f64>, Vec<f64>)> {
if self.source_strengths.is_empty() {
return Err(IntegrateError::ComputationError(
"Panel method not yet solved; call solve() first".to_string(),
));
}
let v_inf_sq =
self.cfg.free_stream[0].powi(2) + self.cfg.free_stream[1].powi(2);
if v_inf_sq < 1e-30 {
return Err(IntegrateError::InvalidInput(
"Free-stream speed is zero".to_string(),
));
}
let mut velocities = Vec::with_capacity(self.mesh.n_elements);
let mut cp = Vec::with_capacity(self.mesh.n_elements);
for elem in &self.mesh.elements {
let v = self.velocity_at(elem.midpoint);
let v_sq = v[0] * v[0] + v[1] * v[1];
velocities.push(v_sq.sqrt());
cp.push(1.0 - v_sq / v_inf_sq);
}
Ok((velocities, cp))
}
pub fn source_strengths(&self) -> &[f64] {
&self.source_strengths
}
pub fn potential_at(&self, p: [f64; 2]) -> f64 {
let mut phi = 0.0_f64;
for (j, elem) in self.mesh.elements.iter().enumerate() {
let sigma_j = self.source_strengths.get(j).copied().unwrap_or(0.0);
phi += sigma_j
* source_panel_g(p, elem.nodes[0], elem.nodes[1], self.cfg.n_gauss);
}
phi + self.cfg.free_stream[0] * p[0] + self.cfg.free_stream[1] * p[1]
}
}
pub(crate) fn gaussian_elimination(
a: &mut Vec<Vec<f64>>,
b: &mut Vec<f64>,
n: usize,
) -> IntegrateResult<Vec<f64>> {
for col in 0..n {
let mut max_row = col;
let mut max_val = a[col][col].abs();
for row in (col + 1)..n {
let v = a[row][col].abs();
if v > max_val {
max_val = v;
max_row = row;
}
}
if max_val < 1e-300 {
return Err(IntegrateError::LinearSolveError(
"Singular or near-singular system in panel method".to_string(),
));
}
a.swap(col, max_row);
b.swap(col, max_row);
let pivot = a[col][col];
for row in (col + 1)..n {
let factor = a[row][col] / pivot;
for k in col..n {
let sub = factor * a[col][k];
a[row][k] -= sub;
}
let sub_b = factor * b[col];
b[row] -= sub_b;
}
}
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)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_panel_method_circular_cylinder() {
let mesh = BoundaryMesh::circle([0.0, 0.0], 1.0, 32);
let cfg = PanelMethodConfig {
n_gauss: 4,
free_stream: [1.0, 0.0],
};
let mut pm = PanelMethod::new(mesh, cfg);
pm.solve().expect("Panel solve failed");
for &sigma in pm.source_strengths() {
assert!(sigma.is_finite(), "Source strength must be finite");
}
}
#[test]
fn test_potential_at_far_field() {
let mesh = BoundaryMesh::circle([0.0, 0.0], 1.0, 16);
let cfg = PanelMethodConfig {
n_gauss: 3,
free_stream: [1.0, 0.0],
};
let mut pm = PanelMethod::new(mesh, cfg);
pm.solve().expect("Panel solve failed");
let phi = pm.potential_at([100.0, 0.0]);
assert!(phi.is_finite());
assert!((phi - 100.0).abs() < 1.0, "Far field potential should be ≈ 100, got {phi}");
}
}