1use super::{AnalyticalSolution, Point, SPEED_OF_SOUND};
7use num_complex::Complex64;
8use std::f64::consts::PI;
9
10pub fn sphere_scattering_3d(
57 wave_number: f64,
58 radius: f64,
59 num_terms: usize,
60 r_points: Vec<f64>,
61 theta_points: Vec<f64>,
62) -> AnalyticalSolution {
63 let ka = wave_number * radius;
64
65 let coefficients = compute_rigid_sphere_coefficients(ka, num_terms);
67
68 let mut positions = Vec::new();
70 let mut pressure = Vec::new();
71
72 for &r in &r_points {
73 for &theta in &theta_points {
74 positions.push(Point::from_spherical(r, theta, 0.0));
75
76 let kr = wave_number * r;
77 let cos_theta = theta.cos();
78
79 let mut total = Complex64::new(0.0, 0.0);
81
82 for (n, coeff) in coefficients.iter().enumerate().take(num_terms) {
83 let n_f64 = n as f64;
84
85 let prefactor = 2.0 * n_f64 + 1.0;
87
88 let i_power_n = Complex64::new((n_f64 * PI / 2.0).cos(), (n_f64 * PI / 2.0).sin());
90
91 let jn = spherical_bessel_j(n, kr);
93
94 let yn = spherical_bessel_y(n, kr);
96 let hn = Complex64::new(jn, yn);
97
98 let pn = legendre_p(n, cos_theta);
100
101 total += prefactor * i_power_n * (jn - coeff * hn) * pn;
103 }
104
105 pressure.push(total);
106 }
107 }
108
109 let frequency = wave_number * SPEED_OF_SOUND / (2.0 * PI);
110
111 AnalyticalSolution {
112 name: format!("3D Sphere Scattering (ka={:.2})", ka),
113 dimensions: 3,
114 positions,
115 pressure,
116 wave_number,
117 frequency,
118 metadata: serde_json::json!({
119 "radius": radius,
120 "ka": ka,
121 "num_terms": num_terms,
122 "boundary_condition": "rigid",
123 "r_points": r_points,
124 "theta_range": [theta_points.first(), theta_points.last()],
125 "regime": classify_regime(ka),
126 }),
127 }
128}
129
130pub fn classify_regime(ka: f64) -> &'static str {
132 if ka < 0.3 {
133 "Rayleigh (ka << 1)"
134 } else if ka < 3.0 {
135 "Mie (ka ~ 1)"
136 } else {
137 "Geometric (ka >> 1)"
138 }
139}
140
141fn compute_rigid_sphere_coefficients(ka: f64, num_terms: usize) -> Vec<Complex64> {
148 let mut coefficients = Vec::with_capacity(num_terms);
149
150 for n in 0..num_terms {
151 let n_f64 = n as f64;
152
153 let jn = spherical_bessel_j(n, ka);
155 let yn = spherical_bessel_y(n, ka);
156
157 let jn_minus_1 = if n > 0 {
159 spherical_bessel_j(n - 1, ka)
160 } else {
161 ka.cos() / ka
163 };
164 let jn_prime = jn_minus_1 - (n_f64 + 1.0) / ka * jn;
165
166 let yn_minus_1 = if n > 0 {
168 spherical_bessel_y(n - 1, ka)
169 } else {
170 -ka.sin() / ka
172 };
173 let yn_prime = yn_minus_1 - (n_f64 + 1.0) / ka * yn;
174
175 let hn_prime = Complex64::new(jn_prime, yn_prime);
177
178 let a_n = Complex64::new(jn_prime, 0.0) / hn_prime;
179
180 coefficients.push(a_n);
181 }
182
183 coefficients
184}
185
186pub fn spherical_bessel_j(n: usize, x: f64) -> f64 {
192 if x.abs() < 1e-10 {
193 return if n == 0 { 1.0 } else { 0.0 };
194 }
195
196 match n {
198 0 => x.sin() / x,
199 1 => x.sin() / (x * x) - x.cos() / x,
200 _ => {
201 let start_n = n + (x.abs() as usize) + 20;
203
204 let mut j_next = 0.0;
205 let mut j_curr = 1e-30;
206
207 let mut values = vec![0.0; start_n + 1];
208 values[start_n] = j_curr;
209
210 for k in (0..start_n).rev() {
211 let j_prev = (2 * k + 3) as f64 / x * j_curr - j_next;
212 values[k] = j_prev;
213 j_next = j_curr;
214 j_curr = j_prev;
215 }
216
217 let true_j0 = x.sin() / x;
219 let scale = true_j0 / values[0];
220
221 values[n] * scale
222 }
223 }
224}
225
226pub fn spherical_bessel_y(n: usize, x: f64) -> f64 {
230 if x.abs() < 1e-10 {
231 return f64::NEG_INFINITY;
232 }
233
234 match n {
235 0 => -x.cos() / x,
236 1 => -x.cos() / (x * x) - x.sin() / x,
237 _ => {
238 let mut y_nm2 = -x.cos() / x;
240 let mut y_nm1 = -x.cos() / (x * x) - x.sin() / x;
241
242 for k in 2..=n {
243 let y_n = (2 * k - 1) as f64 / x * y_nm1 - y_nm2;
244 y_nm2 = y_nm1;
245 y_nm1 = y_n;
246 }
247
248 y_nm1
249 }
250 }
251}
252
253pub fn legendre_p(n: usize, x: f64) -> f64 {
257 match n {
258 0 => 1.0,
259 1 => x,
260 _ => {
261 let mut p_nm2 = 1.0;
262 let mut p_nm1 = x;
263
264 for k in 2..=n {
265 let p_n = ((2 * k - 1) as f64 * x * p_nm1 - (k - 1) as f64 * p_nm2) / k as f64;
266 p_nm2 = p_nm1;
267 p_nm1 = p_n;
268 }
269
270 p_nm1
271 }
272 }
273}
274
275pub fn sphere_rcs_3d(wave_number: f64, radius: f64, num_terms: usize) -> f64 {
279 let ka = wave_number * radius;
280 let coefficients = compute_rigid_sphere_coefficients(ka, num_terms);
281
282 let mut rcs = 0.0;
283 for (n, a_n) in coefficients.iter().enumerate() {
284 rcs += (2 * n + 1) as f64 * a_n.norm_sqr();
285 }
286
287 4.0 * PI * rcs / (wave_number * wave_number)
288}
289
290pub fn sphere_scattering_efficiency_3d(wave_number: f64, radius: f64, num_terms: usize) -> f64 {
294 let rcs = sphere_rcs_3d(wave_number, radius, num_terms);
295 rcs / (PI * radius * radius)
296}
297
298pub fn plane_wave_3d(
309 wave_number: f64,
310 theta: f64,
311 phi: f64,
312 points: Vec<Point>,
313) -> AnalyticalSolution {
314 let kx = wave_number * theta.sin() * phi.cos();
316 let ky = wave_number * theta.sin() * phi.sin();
317 let kz = wave_number * theta.cos();
318
319 let pressure: Vec<Complex64> = points
320 .iter()
321 .map(|p| {
322 let phase = kx * p.x + ky * p.y + kz * p.z;
323 Complex64::new(phase.cos(), phase.sin())
324 })
325 .collect();
326
327 let frequency = wave_number * SPEED_OF_SOUND / (2.0 * PI);
328
329 AnalyticalSolution {
330 name: format!(
331 "3D Plane Wave (k={}, θ={:.2}, φ={:.2})",
332 wave_number, theta, phi
333 ),
334 dimensions: 3,
335 positions: points,
336 pressure,
337 wave_number,
338 frequency,
339 metadata: serde_json::json!({
340 "direction_theta": theta,
341 "direction_phi": phi,
342 "wave_vector": [kx, ky, kz],
343 "wavelength": 2.0 * PI / wave_number,
344 }),
345 }
346}
347
348pub fn point_source_3d(wave_number: f64, source: Point, points: Vec<Point>) -> AnalyticalSolution {
358 let pressure: Vec<Complex64> = points
359 .iter()
360 .map(|p| {
361 let r = p.distance_to(&source);
362 if r < 1e-15 {
363 Complex64::new(f64::INFINITY, 0.0)
364 } else {
365 let kr = wave_number * r;
366 Complex64::new(kr.cos(), kr.sin()) / (4.0 * PI * r)
367 }
368 })
369 .collect();
370
371 let frequency = wave_number * SPEED_OF_SOUND / (2.0 * PI);
372
373 AnalyticalSolution {
374 name: format!("3D Point Source (k={})", wave_number),
375 dimensions: 3,
376 positions: points,
377 pressure,
378 wave_number,
379 frequency,
380 metadata: serde_json::json!({
381 "source": [source.x, source.y, source.z],
382 }),
383 }
384}
385
386#[cfg(test)]
387mod tests {
388 use super::*;
389 use approx::assert_abs_diff_eq;
390
391 #[test]
392 fn test_spherical_bessel_j0() {
393 let x = 1.0;
395 assert_abs_diff_eq!(spherical_bessel_j(0, x), x.sin() / x, epsilon = 1e-10);
396
397 let x = PI;
398 assert_abs_diff_eq!(spherical_bessel_j(0, x), 0.0, epsilon = 1e-10);
399 }
400
401 #[test]
402 fn test_spherical_bessel_j1() {
403 let x: f64 = 2.0;
405 let expected = x.sin() / (x * x) - x.cos() / x;
406 assert_abs_diff_eq!(spherical_bessel_j(1, x), expected, epsilon = 1e-10);
407 }
408
409 #[test]
410 fn test_spherical_bessel_y0() {
411 let x = 1.0;
413 assert_abs_diff_eq!(spherical_bessel_y(0, x), -x.cos() / x, epsilon = 1e-10);
414 }
415
416 #[test]
417 fn test_legendre_polynomials() {
418 assert_abs_diff_eq!(legendre_p(0, 0.5), 1.0, epsilon = 1e-10);
420
421 assert_abs_diff_eq!(legendre_p(1, 0.5), 0.5, epsilon = 1e-10);
423
424 let x = 0.5;
426 let expected = (3.0 * x * x - 1.0) / 2.0;
427 assert_abs_diff_eq!(legendre_p(2, x), expected, epsilon = 1e-10);
428 }
429
430 #[test]
431 fn test_sphere_rayleigh_scattering() {
432 let k = 0.1;
434 let a = 1.0;
435 let ka = k * a;
436
437 let rcs = sphere_rcs_3d(k, a, 10);
438
439 assert!(rcs > 0.0);
441 assert!(rcs.is_finite());
442
443 let rayleigh_scaling = ka.powi(4);
445 assert!(rcs < rayleigh_scaling * 1000.0); }
447
448 #[test]
449 fn test_sphere_geometric_limit() {
450 let k = 20.0;
453 let a = 1.0;
454
455 let rcs = sphere_rcs_3d(k, a, 50);
456 let geometric = 2.0 * PI * a * a;
457
458 assert!((rcs / geometric - 1.0).abs() < 0.2); }
461
462 #[test]
463 fn test_sphere_scattering_3d() {
464 let k = 1.0;
466 let a = 1.0;
467
468 let solution = sphere_scattering_3d(k, a, 20, vec![2.0], vec![0.0, PI / 2.0, PI]);
469
470 assert_eq!(solution.pressure.len(), 3);
471
472 for p in &solution.pressure {
474 assert!(p.re.is_finite());
475 assert!(p.im.is_finite());
476 }
477 }
478
479 #[test]
480 fn test_scattering_efficiency() {
481 let k = 1.0;
482 let a = 1.0;
483
484 let q_scat = sphere_scattering_efficiency_3d(k, a, 30);
485
486 assert!(q_scat > 0.0);
488 assert!(q_scat < 10.0);
489 }
490
491 #[test]
492 fn test_regime_classification() {
493 assert_eq!(classify_regime(0.1), "Rayleigh (ka << 1)");
494 assert_eq!(classify_regime(1.0), "Mie (ka ~ 1)");
495 assert_eq!(classify_regime(10.0), "Geometric (ka >> 1)");
496 }
497
498 #[test]
499 fn test_point_source_3d() {
500 let k = 1.0;
501 let source = Point::new_3d(0.0, 0.0, 0.0);
502 let points = vec![Point::new_3d(1.0, 0.0, 0.0), Point::new_3d(2.0, 0.0, 0.0)];
503
504 let solution = point_source_3d(k, source, points);
505
506 let ratio = solution.pressure[1].norm() / solution.pressure[0].norm();
508 assert_abs_diff_eq!(ratio, 0.5, epsilon = 1e-10);
509 }
510
511 #[test]
512 fn test_plane_wave_3d() {
513 let k = 1.0;
514 let theta = 0.0; let phi = 0.0;
516
517 let points = vec![Point::new_3d(0.0, 0.0, 0.0), Point::new_3d(0.0, 0.0, 1.0)];
518
519 let solution = plane_wave_3d(k, theta, phi, points);
520
521 for p in &solution.pressure {
523 assert_abs_diff_eq!(p.norm(), 1.0, epsilon = 1e-10);
524 }
525 }
526}