scirs2_integrate/bem/
panel_method.rs1use super::boundary_mesh::{gauss_legendre_1d, BoundaryMesh};
13use crate::error::{IntegrateError, IntegrateResult};
14use std::f64::consts::PI;
15
16#[derive(Debug, Clone)]
22pub struct PanelMethodConfig {
23 pub n_gauss: usize,
25 pub free_stream: [f64; 2],
27}
28
29impl Default for PanelMethodConfig {
30 fn default() -> Self {
31 Self {
32 n_gauss: 5,
33 free_stream: [1.0, 0.0],
34 }
35 }
36}
37
38fn source_panel_g(x: [f64; 2], p1: [f64; 2], p2: [f64; 2], n_gauss: usize) -> f64 {
47 let (xi_nodes, weights) = gauss_legendre_1d(n_gauss);
48 let half_len = {
49 let dx = p2[0] - p1[0];
50 let dy = p2[1] - p1[1];
51 (dx * dx + dy * dy).sqrt() * 0.5
52 };
53
54 xi_nodes
55 .iter()
56 .zip(weights.iter())
57 .map(|(&xi, &w)| {
58 let t = (1.0 + xi) * 0.5;
59 let y = [p1[0] + t * (p2[0] - p1[0]), p1[1] + t * (p2[1] - p1[1])];
60 let r = ((x[0] - y[0]).powi(2) + (x[1] - y[1]).powi(2)).sqrt();
61 if r < 1e-14 {
62 0.0
63 } else {
64 -r.ln() / (2.0 * PI) * w * half_len
65 }
66 })
67 .sum()
68}
69
70fn doublet_panel_h(
72 x: [f64; 2],
73 p1: [f64; 2],
74 p2: [f64; 2],
75 normal: [f64; 2],
76 n_gauss: usize,
77) -> f64 {
78 let (xi_nodes, weights) = gauss_legendre_1d(n_gauss);
79 let dx_panel = p2[0] - p1[0];
80 let dy_panel = p2[1] - p1[1];
81 let len = (dx_panel * dx_panel + dy_panel * dy_panel).sqrt();
82 let half_len = len * 0.5;
83
84 xi_nodes
85 .iter()
86 .zip(weights.iter())
87 .map(|(&xi, &w)| {
88 let t = (1.0 + xi) * 0.5;
89 let y = [p1[0] + t * dx_panel, p1[1] + t * dy_panel];
90 let drx = x[0] - y[0];
91 let dry = x[1] - y[1];
92 let r2 = drx * drx + dry * dry;
93 if r2 < 1e-28 {
94 0.0
95 } else {
96 (drx * normal[0] + dry * normal[1]) / (2.0 * PI * r2) * w * half_len
97 }
98 })
99 .sum()
100}
101
102pub struct PanelMethod {
115 mesh: BoundaryMesh,
116 cfg: PanelMethodConfig,
117 source_strengths: Vec<f64>,
119}
120
121impl PanelMethod {
122 pub fn new(mesh: BoundaryMesh, cfg: PanelMethodConfig) -> Self {
124 Self {
125 mesh,
126 cfg,
127 source_strengths: Vec::new(),
128 }
129 }
130
131 fn assemble_influence_matrix(&self) -> Vec<Vec<f64>> {
134 let n = self.mesh.n_elements;
135 let mut a = vec![vec![0.0_f64; n]; n];
136 for i in 0..n {
137 let xi = self.mesh.elements[i].midpoint;
138 for j in 0..n {
139 let ej = &self.mesh.elements[j];
140 let val =
141 doublet_panel_h(xi, ej.nodes[0], ej.nodes[1], ej.normal, self.cfg.n_gauss);
142 a[i][j] = if i == j { val - 0.5 } else { val };
144 }
145 }
146 a
147 }
148
149 pub fn solve(&mut self) -> IntegrateResult<()> {
154 let n = self.mesh.n_elements;
155 let mut a = self.assemble_influence_matrix();
156 let u_inf = self.cfg.free_stream;
157
158 let mut b: Vec<f64> = self
159 .mesh
160 .elements
161 .iter()
162 .map(|e| -(u_inf[0] * e.normal[0] + u_inf[1] * e.normal[1]))
163 .collect();
164
165 let sigma = gaussian_elimination(&mut a, &mut b, n)?;
167 self.source_strengths = sigma;
168 Ok(())
169 }
170
171 pub fn velocity_at(&self, p: [f64; 2]) -> [f64; 2] {
175 let mut vx = 0.0_f64;
176 let mut vy = 0.0_f64;
177 let (xi_nodes, weights) = gauss_legendre_1d(self.cfg.n_gauss);
178
179 for (j, elem) in self.mesh.elements.iter().enumerate() {
180 let sigma_j = self.source_strengths.get(j).copied().unwrap_or(0.0);
181 let half = elem.length * 0.5;
182 let p1 = elem.nodes[0];
183 let p2 = elem.nodes[1];
184
185 for (&xi, &w) in xi_nodes.iter().zip(weights.iter()) {
186 let t = (1.0 + xi) * 0.5;
187 let y = [p1[0] + t * (p2[0] - p1[0]), p1[1] + t * (p2[1] - p1[1])];
188 let dx = p[0] - y[0];
189 let dy = p[1] - y[1];
190 let r2 = dx * dx + dy * dy;
191 if r2 < 1e-28 {
192 continue;
193 }
194 let factor = sigma_j * w * half / (2.0 * PI * r2);
196 vx += factor * dx;
197 vy += factor * dy;
198 }
199 }
200 [vx + self.cfg.free_stream[0], vy + self.cfg.free_stream[1]]
201 }
202
203 pub fn surface_cp(&self) -> IntegrateResult<(Vec<f64>, Vec<f64>)> {
208 if self.source_strengths.is_empty() {
209 return Err(IntegrateError::ComputationError(
210 "Panel method not yet solved; call solve() first".to_string(),
211 ));
212 }
213 let v_inf_sq = self.cfg.free_stream[0].powi(2) + self.cfg.free_stream[1].powi(2);
214 if v_inf_sq < 1e-30 {
215 return Err(IntegrateError::InvalidInput(
216 "Free-stream speed is zero".to_string(),
217 ));
218 }
219
220 let mut velocities = Vec::with_capacity(self.mesh.n_elements);
221 let mut cp = Vec::with_capacity(self.mesh.n_elements);
222
223 for elem in &self.mesh.elements {
224 let v = self.velocity_at(elem.midpoint);
225 let v_sq = v[0] * v[0] + v[1] * v[1];
226 velocities.push(v_sq.sqrt());
227 cp.push(1.0 - v_sq / v_inf_sq);
229 }
230 Ok((velocities, cp))
231 }
232
233 pub fn source_strengths(&self) -> &[f64] {
235 &self.source_strengths
236 }
237
238 pub fn potential_at(&self, p: [f64; 2]) -> f64 {
240 let mut phi = 0.0_f64;
241 for (j, elem) in self.mesh.elements.iter().enumerate() {
242 let sigma_j = self.source_strengths.get(j).copied().unwrap_or(0.0);
243 phi += sigma_j * source_panel_g(p, elem.nodes[0], elem.nodes[1], self.cfg.n_gauss);
244 }
245 phi + self.cfg.free_stream[0] * p[0] + self.cfg.free_stream[1] * p[1]
247 }
248}
249
250pub(crate) fn gaussian_elimination(
256 a: &mut [Vec<f64>],
257 b: &mut [f64],
258 n: usize,
259) -> IntegrateResult<Vec<f64>> {
260 for col in 0..n {
261 let mut max_row = col;
263 let mut max_val = a[col][col].abs();
264 for row in (col + 1)..n {
265 let v = a[row][col].abs();
266 if v > max_val {
267 max_val = v;
268 max_row = row;
269 }
270 }
271 if max_val < 1e-300 {
272 return Err(IntegrateError::LinearSolveError(
273 "Singular or near-singular system in panel method".to_string(),
274 ));
275 }
276 a.swap(col, max_row);
277 b.swap(col, max_row);
278
279 let pivot = a[col][col];
280 for row in (col + 1)..n {
281 let factor = a[row][col] / pivot;
282 for k in col..n {
283 let sub = factor * a[col][k];
284 a[row][k] -= sub;
285 }
286 let sub_b = factor * b[col];
287 b[row] -= sub_b;
288 }
289 }
290 let mut x = vec![0.0_f64; n];
292 for i in (0..n).rev() {
293 let mut s = b[i];
294 for j in (i + 1)..n {
295 s -= a[i][j] * x[j];
296 }
297 x[i] = s / a[i][i];
298 }
299 Ok(x)
300}
301
302#[cfg(test)]
307mod tests {
308 use super::*;
309
310 #[test]
311 fn test_panel_method_circular_cylinder() {
312 let mesh = BoundaryMesh::circle([0.0, 0.0], 1.0, 32);
315 let cfg = PanelMethodConfig {
316 n_gauss: 4,
317 free_stream: [1.0, 0.0],
318 };
319 let mut pm = PanelMethod::new(mesh, cfg);
320 pm.solve().expect("Panel solve failed");
321
322 for &sigma in pm.source_strengths() {
324 assert!(sigma.is_finite(), "Source strength must be finite");
325 }
326 }
327
328 #[test]
329 fn test_potential_at_far_field() {
330 let mesh = BoundaryMesh::circle([0.0, 0.0], 1.0, 16);
331 let cfg = PanelMethodConfig {
332 n_gauss: 3,
333 free_stream: [1.0, 0.0],
334 };
335 let mut pm = PanelMethod::new(mesh, cfg);
336 pm.solve().expect("Panel solve failed");
337
338 let phi = pm.potential_at([100.0, 0.0]);
340 assert!(phi.is_finite());
341 assert!(
343 (phi - 100.0).abs() < 1.0,
344 "Far field potential should be ≈ 100, got {phi}"
345 );
346 }
347}