scirs2_special/spheroidal/
wave_functions.rs1use std::f64::consts::PI;
8
9use crate::error::{SpecialError, SpecialResult};
10use crate::mathieu::advanced::{tridiag_eigenvalues, tridiag_eigenvector};
11
12#[non_exhaustive]
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum SpheroidType {
16 Prolate,
18 Oblate,
20}
21
22#[derive(Debug, Clone)]
24pub struct SpheroidalConfig {
25 pub n_expansion: usize,
27 pub tol: f64,
29}
30
31impl Default for SpheroidalConfig {
32 fn default() -> Self {
33 SpheroidalConfig {
34 n_expansion: 40,
35 tol: 1e-12,
36 }
37 }
38}
39
40pub fn associated_legendre(l: usize, m: usize, x: f64) -> f64 {
59 if m > l {
60 return 0.0;
61 }
62
63 let factor = (1.0 - x * x).max(0.0).sqrt();
65 let mut pmm = 1.0_f64;
66 for i in 0..m {
68 pmm *= -((2 * i + 1) as f64) * factor;
69 }
70 if l == m {
73 return pmm;
74 }
75
76 let mut pmm1 = x * (2 * m + 1) as f64 * pmm;
78 if l == m + 1 {
79 return pmm1;
80 }
81
82 let mut pl_prev = pmm;
84 let mut pl = pmm1;
85 for k in (m + 1)..l {
86 let pk1 = ((2 * k + 1) as f64 * x * pl - (k + m) as f64 * pl_prev) / (k - m + 1) as f64;
87 pl_prev = pl;
88 pl = pk1;
89 }
90 let _ = pmm1; pl
92}
93
94fn build_spheroidal_tridiag(
112 m: usize,
113 n: usize,
114 c: f64,
115 config: &SpheroidalConfig,
116) -> (Vec<f64>, Vec<f64>) {
117 let c2 = c * c;
118 let parity = (n - m) % 2; let nf = config.n_expansion;
120
121 let mut diag = vec![0.0_f64; nf];
122 let mut off = vec![0.0_f64; nf.saturating_sub(1)];
123
124 for p in 0..nf {
126 let k = 2 * p + parity; let ell = (m + k) as f64; let alpha_p = if k >= 2 {
134 let ll = ell; -c2 * (ll + m as f64) * (ll + m as f64 - 1.0) / ((2.0 * ll - 1.0) * (2.0 * ll + 1.0))
136 } else {
137 0.0
138 };
139 let gamma_p = -c2 * (ell - m as f64 + 1.0) * (ell - m as f64 + 2.0)
140 / ((2.0 * ell + 1.0) * (2.0 * ell + 3.0));
141
142 diag[p] = ell * (ell + 1.0) + alpha_p + gamma_p;
143
144 if p + 1 < nf {
146 let ll = ell;
147 let off_val = -c2 * (ll - m as f64 + 1.0) * (ll - m as f64 + 2.0)
148 / ((2.0 * ll + 1.0) * (2.0 * ll + 3.0));
149 off[p] = off_val;
150 }
151 }
152
153 (diag, off)
154}
155
156pub fn spheroidal_eigenvalue(
170 m: usize,
171 n: usize,
172 c: f64,
173 stype: &SpheroidType,
174 config: &SpheroidalConfig,
175) -> SpecialResult<f64> {
176 if n < m {
177 return Err(SpecialError::DomainError(format!(
178 "spheroidal_eigenvalue: n={n} must be >= m={m}"
179 )));
180 }
181
182 let c_eff = match stype {
184 SpheroidType::Prolate => c,
185 SpheroidType::Oblate => c, };
187
188 let (mut diag, mut off) = build_spheroidal_tridiag(m, n, c_eff, config);
189
190 if matches!(stype, SpheroidType::Oblate) {
192 let (diag0, off0) = build_spheroidal_tridiag(m, n, 0.0, config);
193 for (i, (d, d0)) in diag.iter_mut().zip(diag0.iter()).enumerate() {
194 *d = 2.0 * d0 - *d; let _ = i;
196 }
197 for (o, o0) in off.iter_mut().zip(off0.iter()) {
198 *o = 2.0 * o0 - *o;
199 }
200 }
201
202 let eigenvalues = tridiag_eigenvalues(&diag, &off);
203
204 let target = (n - m) / 2;
206 eigenvalues.get(target).copied().ok_or_else(|| {
207 SpecialError::ComputationError(format!(
208 "spheroidal_eigenvalue: failed to find eigenvalue for m={m}, n={n}, c={c}"
209 ))
210 })
211}
212
213pub fn angular_spheroidal(
233 m: usize,
234 n: usize,
235 c: f64,
236 x: f64,
237 stype: &SpheroidType,
238 config: &SpheroidalConfig,
239) -> SpecialResult<f64> {
240 if n < m {
241 return Err(SpecialError::DomainError(format!(
242 "angular_spheroidal: n={n} must be >= m={m}"
243 )));
244 }
245 if x.abs() > 1.0 + 1e-12 {
246 return Err(SpecialError::DomainError(format!(
247 "angular_spheroidal: x={x} must satisfy |x| <= 1"
248 )));
249 }
250 let x_clamped = x.clamp(-1.0, 1.0);
251
252 let (diag, off) = build_spheroidal_tridiag(m, n, c, config);
254 let eigenvalues = tridiag_eigenvalues(&diag, &off);
255 let target = (n - m) / 2;
256 let eigenval = eigenvalues.get(target).copied().ok_or_else(|| {
257 SpecialError::ComputationError(format!(
258 "angular_spheroidal: no eigenvalue for m={m}, n={n}"
259 ))
260 })?;
261 let coeffs = tridiag_eigenvector(&diag, &off, eigenval);
262
263 let parity = (n - m) % 2;
264
265 let mut result = 0.0_f64;
267 for (p, &d) in coeffs.iter().enumerate() {
268 let k = 2 * p + parity;
269 let l = m + k;
270 let p_val = associated_legendre(l, m, x_clamped);
271 result += d * p_val;
272 }
273
274 let _ = stype;
276
277 Ok(result)
278}
279
280pub fn spherical_bessel_j(l: usize, x: f64) -> f64 {
297 if x.abs() < 1e-300 {
298 if l == 0 {
299 return 1.0; } else {
301 return 0.0;
302 }
303 }
304
305 let j0 = x.sin() / x;
306 if l == 0 {
307 return j0;
308 }
309 let j1 = x.sin() / (x * x) - x.cos() / x;
310 if l == 1 {
311 return j1;
312 }
313
314 let mut jm1 = j0;
316 let mut jc = j1;
317 for k in 1..l {
318 let jp1 = (2 * k + 1) as f64 / x * jc - jm1;
319 jm1 = jc;
320 jc = jp1;
321 }
322 jc
323}
324
325pub fn radial_spheroidal_1(
346 m: usize,
347 n: usize,
348 c: f64,
349 xi: f64,
350 config: &SpheroidalConfig,
351) -> SpecialResult<f64> {
352 if n < m {
353 return Err(SpecialError::DomainError(format!(
354 "radial_spheroidal_1: n={n} must be >= m={m}"
355 )));
356 }
357 if xi < 1.0 - 1e-12 {
358 return Err(SpecialError::DomainError(format!(
359 "radial_spheroidal_1: xi={xi} must be >= 1"
360 )));
361 }
362
363 let (diag, off) = build_spheroidal_tridiag(m, n, c, config);
364 let eigenvalues = tridiag_eigenvalues(&diag, &off);
365 let target = (n - m) / 2;
366 let eigenval = eigenvalues.get(target).copied().ok_or_else(|| {
367 SpecialError::ComputationError(format!(
368 "radial_spheroidal_1: no eigenvalue for m={m}, n={n}"
369 ))
370 })?;
371 let coeffs = tridiag_eigenvector(&diag, &off, eigenval);
372
373 let parity = (n - m) % 2;
374
375 let norm_idx = target; let d_norm = if norm_idx < coeffs.len() {
378 let v = coeffs[norm_idx];
379 if v.abs() < 1e-15 {
380 1.0
381 } else {
382 v
383 }
384 } else {
385 1.0
386 };
387
388 let xi2m1 = (xi * xi - 1.0).max(0.0);
390 let factor = xi2m1.powf(m as f64 / 2.0);
391
392 let mut sum = 0.0_f64;
394 for (p, &d) in coeffs.iter().enumerate() {
395 let k = 2 * p + parity;
396 let l = m + k;
397 let jl = spherical_bessel_j(l, c * xi);
398 sum += d * jl;
399 }
400
401 Ok(factor * sum / d_norm)
402}
403
404#[cfg(test)]
405mod tests {
406 use super::*;
407 use std::f64::consts::PI;
408
409 #[test]
410 fn test_associated_legendre_p00() {
411 for &x in &[-0.5, 0.0, 0.5, 1.0] {
413 let val = associated_legendre(0, 0, x);
414 assert!(
415 (val - 1.0).abs() < 1e-14,
416 "P_0^0({x}) should be 1, got {val}"
417 );
418 }
419 }
420
421 #[test]
422 fn test_associated_legendre_p10() {
423 for &x in &[-0.7, -0.3, 0.0, 0.4, 0.8] {
425 let val = associated_legendre(1, 0, x);
426 assert!(
427 (val - x).abs() < 1e-14,
428 "P_1^0({x}) should be {x}, got {val}"
429 );
430 }
431 }
432
433 #[test]
434 fn test_associated_legendre_p11() {
435 for &x in &[-0.7, -0.3, 0.0, 0.4, 0.8] {
437 let val = associated_legendre(1, 1, x);
438 let expected = -(1.0 - x * x).sqrt();
439 assert!(
440 (val - expected).abs() < 1e-13,
441 "P_1^1({x}) should be {expected}, got {val}"
442 );
443 }
444 }
445
446 #[test]
447 fn test_associated_legendre_p20() {
448 for &x in &[-0.7, 0.0, 0.5, 1.0] {
450 let val = associated_legendre(2, 0, x);
451 let expected = (3.0 * x * x - 1.0) / 2.0;
452 assert!(
453 (val - expected).abs() < 1e-13,
454 "P_2^0({x}) = {expected}, got {val}"
455 );
456 }
457 }
458
459 #[test]
460 fn test_spherical_bessel_j0_at_pi() {
461 let val = spherical_bessel_j(0, PI);
463 assert!(val.abs() < 1e-14, "j_0(π) ≈ 0, got {val}");
464 }
465
466 #[test]
467 fn test_spherical_bessel_j0_near_zero() {
468 let val = spherical_bessel_j(0, 1e-300);
470 assert!((val - 1.0).abs() < 1e-12, "j_0(0) → 1, got {val}");
471 }
472
473 #[test]
474 fn test_spherical_bessel_j1() {
475 let x = 1.5;
477 let val = spherical_bessel_j(1, x);
478 let expected = x.sin() / (x * x) - x.cos() / x;
479 assert!(
480 (val - expected).abs() < 1e-13,
481 "j_1({x}) = {expected}, got {val}"
482 );
483 }
484
485 #[test]
486 fn test_spheroidal_eigenvalue_c_zero() {
487 let config = SpheroidalConfig::default();
489 for n in [2usize, 3, 4] {
490 let m = 0;
491 let lam = spheroidal_eigenvalue(m, n, 0.0, &SpheroidType::Prolate, &config).unwrap();
492 let expected = (n * (n + 1)) as f64;
493 assert!(
494 (lam - expected).abs() < 0.5,
495 "λ_{{m={m},n={n}}}(0) should be n(n+1)={expected}, got {lam}"
496 );
497 }
498 }
499
500 #[test]
501 fn test_angular_spheroidal_c_zero_legendre() {
502 let config = SpheroidalConfig {
504 n_expansion: 20,
505 tol: 1e-12,
506 };
507 let x = 0.5;
508 let s = angular_spheroidal(0, 2, 0.0, x, &SpheroidType::Prolate, &config).unwrap();
509 assert!(
512 s.is_finite(),
513 "angular_spheroidal c=0 should be finite, got {s}"
514 );
515 }
516
517 #[test]
518 fn test_radial_spheroidal_1_finite() {
519 let config = SpheroidalConfig::default();
520 let val = radial_spheroidal_1(0, 2, 1.0, 1.5, &config).unwrap();
521 assert!(
522 val.is_finite(),
523 "radial_spheroidal_1 should be finite, got {val}"
524 );
525 }
526}