Skip to main content

scirs2_special/
spherical_harmonics.rs

1//! Spherical Harmonics
2//!
3//! This module provides implementations of spherical harmonic functions Y_l^m(θ, φ)
4//! that are important in quantum mechanics, physical chemistry, and solving
5//! differential equations in spherical coordinates.
6
7use crate::error::SpecialResult;
8use scirs2_core::numeric::{Float, FromPrimitive};
9use std::f64;
10use std::f64::consts::PI;
11use std::fmt::Debug;
12
13/// Computes the value of the real spherical harmonic Y_l^m(θ, φ) function.
14///
15/// The spherical harmonics Y_l^m(θ, φ) form a complete orthogonal basis for functions
16/// defined on the sphere. They appear extensively in solving three-dimensional
17/// partial differential equations in spherical coordinates, especially in quantum physics.
18///
19/// This implementation returns the real form of the spherical harmonic, which is often
20/// more convenient for practical applications.
21///
22/// # Arguments
23///
24/// * `l` - Degree (non-negative integer)
25/// * `m` - Order (integer with |m| ≤ l)
26/// * `theta` - Polar angle (in radians, 0 ≤ θ ≤ π)
27/// * `phi` - Azimuthal angle (in radians, 0 ≤ φ < 2π)
28///
29/// # Returns
30///
31/// * Value of the real spherical harmonic Y_l^m(θ, φ)
32///
33/// # Examples
34///
35/// ```
36/// use scirs2_special::sph_harm;
37/// use std::f64::consts::PI;
38///
39/// // Y₀⁰(θ, φ) = 1/(2√π)
40/// let y00: f64 = sph_harm(0, 0, PI/2.0, 0.0).expect("Operation failed");
41/// assert!((y00 - 0.5/f64::sqrt(PI)).abs() < 1e-10);
42///
43/// // Y₁⁰(θ, φ) = √(3/4π) cos(θ)
44/// let y10: f64 = sph_harm(1, 0, PI/4.0, 0.0).expect("Operation failed");
45/// let expected = f64::sqrt(3.0/(4.0*PI)) * f64::cos(PI/4.0);
46/// assert!((y10 - expected).abs() < 1e-10);
47/// ```
48#[allow(dead_code)]
49pub fn sph_harm<F>(l: usize, m: i32, theta: F, phi: F) -> SpecialResult<F>
50where
51    F: Float + FromPrimitive + Debug,
52{
53    // Validate that |m| <= l
54    let m_abs = m.unsigned_abs() as usize;
55    if m_abs > l {
56        return Ok(F::zero()); // Y_l^m = 0 if |m| > l
57    }
58
59    let cos_theta = theta.cos();
60    let x = cos_theta.to_f64().unwrap_or(0.0);
61
62    // Compute K_l^|m| * P_l^|m|(cos theta) directly via fully-normalized recurrence
63    let bar_plm = normalized_assoc_legendre(l, m_abs, x);
64    let bar_plm_f = F::from(bar_plm).unwrap_or(F::zero());
65
66    // Compute angular part for real spherical harmonics
67    let angular_part: F;
68    if m == 0 {
69        angular_part = F::one();
70    } else {
71        let sqrt2 = F::from(std::f64::consts::SQRT_2).unwrap_or(F::one());
72        let m_f = F::from(m_abs).unwrap_or(F::one());
73        let m_phi = m_f * phi;
74
75        if m > 0 {
76            angular_part = sqrt2 * m_phi.cos();
77        } else {
78            angular_part = sqrt2 * m_phi.sin();
79        }
80    }
81
82    Ok(bar_plm_f * angular_part)
83}
84
85/// Compute K_l^m * P_l^m(x) directly via fully-normalized recurrence.
86///
87/// Returns `bar_P_l^m(x) = K_l^m * P_l^m(x)` where
88/// `K_l^m = sqrt((2l+1)/(4π) * (l-m)!/(l+m)!)`.
89///
90/// NO Condon-Shortley phase is included (same convention as the old
91/// `associated_legendre_for_sph`).
92///
93/// The key advantage of this formulation is that it avoids computing
94/// the un-normalised P_l^m and the normalization constant separately,
95/// which would overflow for large l=m (e.g., l=m=150 at theta=PI).
96/// Instead, each factor in the seed product is ≤ 1, preventing overflow.
97fn normalized_assoc_legendre(l: usize, m: usize, x: f64) -> f64 {
98    if m > l {
99        return 0.0;
100    }
101
102    let x = x.clamp(-1.0, 1.0);
103    let sin_theta = ((1.0 - x) * (1.0 + x)).sqrt();
104
105    // Seed: bar_P_m^m = sqrt(1/(4π)) * prod_{k=1}^{m} sqrt((2k-1)/(2k)) * sin_theta^m * sqrt(2m+1)
106    // Each factor sqrt((2k-1)/(2k)) <= 1, so no overflow.
107    let inv_4pi = 1.0 / (4.0 * std::f64::consts::PI);
108    let mut bar_pmm = inv_4pi.sqrt(); // sqrt(1/(4π))
109    for k in 1..=m {
110        let k_f = k as f64;
111        bar_pmm *= ((2.0 * k_f - 1.0) / (2.0 * k_f)).sqrt() * sin_theta;
112    }
113    bar_pmm *= ((2 * m + 1) as f64).sqrt();
114
115    if l == m {
116        return bar_pmm;
117    }
118
119    // bar_P_{m+1}^m = sqrt(2m+3) * x * bar_P_m^m
120    let mut bar_pm1 = ((2 * m + 3) as f64).sqrt() * x * bar_pmm;
121
122    if l == m + 1 {
123        return bar_pm1;
124    }
125
126    // Three-term recurrence for ll >= m+2:
127    // alpha_ll = sqrt((4*ll^2 - 1) / (ll^2 - m^2))
128    // beta_ll  = sqrt((2*ll+1)*(ll+m-1)*(ll-m-1) / ((2*ll-3)*(ll^2 - m^2)))
129    // bar_P_ll^m = alpha_ll * x * bar_P_{ll-1}^m - beta_ll * bar_P_{ll-2}^m
130    let mut bar_prev2 = bar_pmm;
131    let mut bar_prev1 = bar_pm1;
132    let mut bar_cur = 0.0;
133    let m2 = (m * m) as f64;
134    for ll in (m + 2)..=l {
135        let ll_f = ll as f64;
136        let ll2 = ll_f * ll_f;
137        let denom = ll2 - m2;
138        let alpha = ((4.0 * ll2 - 1.0) / denom).sqrt();
139        let beta = ((2.0 * ll_f + 1.0) * (ll_f + m as f64 - 1.0) * (ll_f - m as f64 - 1.0)
140            / ((2.0 * ll_f - 3.0) * denom))
141            .sqrt();
142        bar_cur = alpha * x * bar_prev1 - beta * bar_prev2;
143        bar_prev2 = bar_prev1;
144        bar_prev1 = bar_cur;
145    }
146
147    bar_cur
148}
149
150/// Computes the value of the complex spherical harmonic Y_l^m(θ, φ) function.
151///
152/// The complex spherical harmonics are the conventional form found in quantum mechanics.
153/// They are eigenfunctions of the angular momentum operators.
154///
155/// # Arguments
156///
157/// * `l` - Degree (non-negative integer)
158/// * `m` - Order (integer with |m| ≤ l)
159/// * `theta` - Polar angle (in radians, 0 ≤ θ ≤ π)
160/// * `phi` - Azimuthal angle (in radians, 0 ≤ φ < 2π)
161///
162/// # Returns
163///
164/// * Real part of the complex spherical harmonic Y_l^m(θ, φ)
165/// * Imaginary part of the complex spherical harmonic Y_l^m(θ, φ)
166///
167/// # Examples
168///
169/// ```
170/// use scirs2_special::sph_harm_complex;
171/// use std::f64::consts::PI;
172///
173/// // Y₀⁰(θ, φ) = 1/(2√π)
174/// let (re, im): (f64, f64) = sph_harm_complex(0, 0, PI/2.0, 0.0).expect("Operation failed");
175/// assert!((re - 0.5/f64::sqrt(PI)).abs() < 1e-10);
176/// assert!(im.abs() < 1e-10);
177///
178/// // Y₁¹(θ, φ) = -√(3/8π) sin(θ) e^(iφ)
179/// let (re, im): (f64, f64) = sph_harm_complex(1, 1, PI/4.0, PI/3.0).expect("Operation failed");
180/// let amplitude = -f64::sqrt(3.0/(8.0*PI)) * f64::sin(PI/4.0);
181/// let expected_re = amplitude * f64::cos(PI/3.0);
182/// let expected_im = amplitude * f64::sin(PI/3.0);
183/// assert!((re - expected_re).abs() < 1e-10);
184/// assert!((im - expected_im).abs() < 1e-10);
185/// ```
186#[allow(dead_code)]
187pub fn sph_harm_complex<F>(l: usize, m: i32, theta: F, phi: F) -> SpecialResult<(F, F)>
188where
189    F: Float + FromPrimitive + Debug,
190{
191    // Validate that |m| <= l
192    let m_abs = m.unsigned_abs() as usize;
193    if m_abs > l {
194        return Ok((F::zero(), F::zero())); // Y_l^m = 0 if |m| > l
195    }
196
197    let cos_theta = theta.cos();
198    let x = cos_theta.to_f64().unwrap_or(0.0);
199
200    // Compute K_l^|m| * P_l^|m|(cos theta) directly via fully-normalized recurrence
201    let bar_plm = normalized_assoc_legendre(l, m_abs, x);
202    let bar_plm_f = F::from(bar_plm).unwrap_or(F::zero());
203
204    // Physics convention: Y_l^m = (-1)^m * K_l^m * P_l^|m|(cos theta) * e^{im*phi}
205    // The (-1)^m is the Condon-Shortley phase applied to the spherical harmonic
206    let cs_phase = if m_abs.is_multiple_of(2) {
207        F::one()
208    } else {
209        -F::one()
210    };
211
212    // For negative m, use: Y_l^{-|m|} = (-1)^|m| * conj(Y_l^{|m|})
213    // Y_l^{|m|} = (-1)^|m| * K * P * e^{i|m|phi}
214    // Y_l^{-|m|} = (-1)^|m| * conj((-1)^|m| * K * P * e^{i|m|phi})
215    //            = (-1)^{2|m|} * K * P * e^{-i|m|phi}
216    //            = K * P * e^{-i|m|phi}
217    //
218    // For positive m:
219    // Y_l^m = (-1)^m * K * P * e^{im*phi}
220    //
221    // For m = 0:
222    // Y_l^0 = K * P
223
224    // Compute e^{im*phi}
225    let m_f64 = m as f64;
226    let m_phi = F::from(m_f64).unwrap_or(F::zero()) * phi;
227    let cos_m_phi = m_phi.cos();
228    let sin_m_phi = m_phi.sin();
229
230    let amplitude = if m >= 0 {
231        cs_phase * bar_plm_f
232    } else {
233        // Y_l^{-|m|} = K * P * e^{-i|m|phi}
234        // e^{im*phi} with m negative already gives e^{-i|m|phi}
235        bar_plm_f
236    };
237
238    let real_part = amplitude * cos_m_phi;
239    let imag_part = amplitude * sin_m_phi;
240
241    Ok((real_part, imag_part))
242}
243
244/// Compute the normalization factor for spherical harmonics.
245///
246/// K_l^m = sqrt[(2l+1)/(4pi) * (l-|m|)!/(l+|m|)!]
247///
248/// This is useful for constructing custom spherical harmonic variants.
249///
250/// # Examples
251/// ```
252/// use scirs2_special::sph_harm_normalization;
253/// use std::f64::consts::PI;
254/// // K_0^0 = 1/(2*sqrt(pi))
255/// let k = sph_harm_normalization(0, 0);
256/// assert!((k - 0.5 / PI.sqrt()).abs() < 1e-14);
257/// ```
258pub fn sph_harm_normalization(l: usize, m: i32) -> f64 {
259    let m_abs = m.unsigned_abs() as usize;
260    if m_abs > l {
261        return 0.0;
262    }
263
264    let two_l_plus_1 = (2 * l + 1) as f64;
265    let four_pi = 4.0 * PI;
266
267    let mut factorial_ratio = 1.0;
268    if m_abs > 0 {
269        for i in (l - m_abs + 1)..=(l + m_abs) {
270            factorial_ratio /= i as f64;
271        }
272    }
273
274    (two_l_plus_1 / four_pi * factorial_ratio).sqrt()
275}
276
277/// Regular solid harmonic R_l^m(r, theta, phi).
278///
279/// Defined as:
280/// ```text
281/// R_l^m(r, theta, phi) = sqrt(4*pi/(2l+1)) * r^l * Y_l^m(theta, phi)
282/// ```
283///
284/// Regular solid harmonics are the harmonic polynomial solutions of
285/// Laplace's equation that are regular at the origin.
286///
287/// # Arguments
288/// * `l` - Degree
289/// * `m` - Order (|m| <= l)
290/// * `r` - Radial distance (r >= 0)
291/// * `theta` - Polar angle
292/// * `phi` - Azimuthal angle
293///
294/// # Examples
295/// ```
296/// use scirs2_special::solid_harmonic_regular;
297/// use std::f64::consts::PI;
298/// // R_0^0 = 1 (independent of r, theta, phi)
299/// let r = solid_harmonic_regular(0, 0, 1.0, PI/4.0, 0.0).expect("failed");
300/// assert!((r - 1.0).abs() < 1e-10);
301/// ```
302pub fn solid_harmonic_regular(
303    l: usize,
304    m: i32,
305    r: f64,
306    theta: f64,
307    phi: f64,
308) -> SpecialResult<f64> {
309    let y_lm: f64 = sph_harm(l, m, theta, phi)?;
310    let factor = (4.0 * PI / (2 * l + 1) as f64).sqrt();
311    Ok(factor * r.powi(l as i32) * y_lm)
312}
313
314/// Irregular solid harmonic I_l^m(r, theta, phi).
315///
316/// Defined as:
317/// ```text
318/// I_l^m(r, theta, phi) = sqrt(4*pi/(2l+1)) * r^{-(l+1)} * Y_l^m(theta, phi)
319/// ```
320///
321/// Irregular solid harmonics are solutions of Laplace's equation that are
322/// singular at the origin but regular at infinity.
323///
324/// # Arguments
325/// * `l` - Degree
326/// * `m` - Order (|m| <= l)
327/// * `r` - Radial distance (r > 0)
328/// * `theta` - Polar angle
329/// * `phi` - Azimuthal angle
330///
331/// # Examples
332/// ```
333/// use scirs2_special::solid_harmonic_irregular;
334/// use std::f64::consts::PI;
335/// // At r=1: I_0^0 = 1/(2*sqrt(pi)) * sqrt(4*pi) = 1
336/// let r = solid_harmonic_irregular(0, 0, 1.0, PI/4.0, 0.0).expect("failed");
337/// assert!((r - 1.0).abs() < 1e-10);
338/// ```
339pub fn solid_harmonic_irregular(
340    l: usize,
341    m: i32,
342    r: f64,
343    theta: f64,
344    phi: f64,
345) -> SpecialResult<f64> {
346    if r <= 0.0 {
347        return Err(crate::SpecialError::DomainError(
348            "r must be > 0 for irregular solid harmonic".to_string(),
349        ));
350    }
351
352    let y_lm: f64 = sph_harm(l, m, theta, phi)?;
353    let factor = (4.0 * PI / (2 * l + 1) as f64).sqrt();
354    Ok(factor / r.powi((l + 1) as i32) * y_lm)
355}
356
357/// Spherical harmonic addition theorem coefficient.
358///
359/// The addition theorem states:
360/// ```text
361/// P_l(cos(gamma)) = (4*pi/(2l+1)) * sum_{m=-l}^{l} Y_l^m*(theta1,phi1) * Y_l^m(theta2,phi2)
362/// ```
363/// where gamma is the angle between the two directions (theta1,phi1) and (theta2,phi2).
364///
365/// This function computes cos(gamma) given two directions.
366///
367/// # Arguments
368/// * `theta1`, `phi1` - First direction
369/// * `theta2`, `phi2` - Second direction
370///
371/// # Returns
372/// cos(gamma) where gamma is the angle between the two directions
373///
374/// # Examples
375/// ```
376/// use scirs2_special::sph_harm_cos_angle;
377/// use std::f64::consts::PI;
378/// // Same direction: cos(gamma) = 1
379/// let cos_g = sph_harm_cos_angle(PI/4.0, 0.0, PI/4.0, 0.0);
380/// assert!((cos_g - 1.0).abs() < 1e-14);
381/// // Opposite directions: cos(gamma) = -1
382/// let cos_g2 = sph_harm_cos_angle(0.0, 0.0, PI, 0.0);
383/// assert!((cos_g2 + 1.0).abs() < 1e-14);
384/// ```
385pub fn sph_harm_cos_angle(theta1: f64, phi1: f64, theta2: f64, phi2: f64) -> f64 {
386    theta1.sin() * theta2.sin() * (phi1 - phi2).cos() + theta1.cos() * theta2.cos()
387}
388
389/// Verify the spherical harmonic addition theorem.
390///
391/// Computes P_l(cos(gamma)) and compares with the sum
392/// (4pi/(2l+1)) * sum_{m=-l}^{l} Y_l^m(theta1,phi1) * Y_l^m(theta2,phi2)
393///
394/// Returns both values as (legendre_value, sum_value).
395pub fn sph_harm_addition_theorem_check(
396    l: usize,
397    theta1: f64,
398    phi1: f64,
399    theta2: f64,
400    phi2: f64,
401) -> SpecialResult<(f64, f64)> {
402    use crate::orthogonal::legendre;
403
404    let cos_gamma = sph_harm_cos_angle(theta1, phi1, theta2, phi2);
405    let p_l = legendre(l, cos_gamma);
406
407    let factor = 4.0 * PI / (2 * l + 1) as f64;
408    let mut sum = 0.0;
409
410    for m_i in -(l as i32)..=(l as i32) {
411        let y1: f64 = sph_harm(l, m_i, theta1, phi1)?;
412        let y2: f64 = sph_harm(l, m_i, theta2, phi2)?;
413        sum += y1 * y2;
414    }
415
416    Ok((p_l, factor * sum))
417}
418
419#[cfg(test)]
420mod tests {
421    use super::*;
422    use approx::assert_relative_eq;
423    use std::f64::consts::{PI, SQRT_2};
424
425    #[test]
426    fn test_real_spherical_harmonics() {
427        // Test Y₀⁰: Y₀⁰(θ, φ) = 1/(2√π)
428        let expected_y00 = 0.5 / f64::sqrt(PI);
429        assert_relative_eq!(
430            sph_harm(0, 0, 0.0, 0.0).expect("Operation failed"),
431            expected_y00,
432            epsilon = 1e-10
433        );
434        assert_relative_eq!(
435            sph_harm(0, 0, PI / 2.0, 0.0).expect("Operation failed"),
436            expected_y00,
437            epsilon = 1e-10
438        );
439        assert_relative_eq!(
440            sph_harm(0, 0, PI, 0.0).expect("Operation failed"),
441            expected_y00,
442            epsilon = 1e-10
443        );
444
445        // Test Y₁⁰: Y₁⁰(θ, φ) = √(3/4π) cos(θ)
446        let factor_y10 = f64::sqrt(3.0 / (4.0 * PI));
447        assert_relative_eq!(
448            sph_harm(1, 0, 0.0, 0.0).expect("Operation failed"),
449            factor_y10,
450            epsilon = 1e-10
451        ); // θ=0, cos(θ)=1
452        assert_relative_eq!(
453            sph_harm(1, 0, PI / 2.0, 0.0).expect("Operation failed"),
454            0.0,
455            epsilon = 1e-10
456        ); // θ=π/2, cos(θ)=0
457        assert_relative_eq!(
458            sph_harm(1, 0, PI, 0.0).expect("Operation failed"),
459            -factor_y10,
460            epsilon = 1e-10
461        ); // θ=π, cos(θ)=-1
462
463        // Test Y₁¹: Y₁¹(θ, φ) = √(3/8π) sin(θ) cos(φ) * √2
464        let factor_y11 = f64::sqrt(3.0 / (8.0 * PI)) * SQRT_2;
465
466        // At (θ=π/2, φ=0): sin(θ)=1, cos(φ)=1
467        assert_relative_eq!(
468            sph_harm(1, 1, PI / 2.0, 0.0).expect("Operation failed"),
469            factor_y11,
470            epsilon = 1e-10
471        );
472
473        // At (θ=π/2, φ=π/2): sin(θ)=1, cos(φ)=0
474        assert_relative_eq!(
475            sph_harm(1, 1, PI / 2.0, PI / 2.0).expect("Operation failed"),
476            0.0,
477            epsilon = 1e-10
478        );
479
480        // Test Y₂⁰: Y₂⁰(θ, φ) = √(5/16π) (3cos²(θ) - 1)
481        let factor_y20 = f64::sqrt(5.0 / (16.0 * PI));
482
483        // At θ=0: cos²(θ)=1, Y₂⁰ = √(5/16π) * 2
484        assert_relative_eq!(
485            sph_harm(2, 0, 0.0, 0.0).expect("Operation failed"),
486            factor_y20 * 2.0,
487            epsilon = 1e-10
488        );
489
490        // Verify that m > l returns zero
491        assert_relative_eq!(
492            sph_harm(1, 2, PI / 2.0, 0.0).expect("Operation failed"),
493            0.0,
494            epsilon = 1e-10
495        );
496    }
497
498    #[test]
499    fn test_complex_spherical_harmonics() {
500        // Test Y₀⁰: Y₀⁰(θ, φ) = 1/(2√π)
501        let expected_y00 = 0.5 / f64::sqrt(PI);
502        let (re, im) = sph_harm_complex(0, 0, 0.0, 0.0).expect("Operation failed");
503        assert_relative_eq!(re, expected_y00, epsilon = 1e-10);
504        assert_relative_eq!(im, 0.0, epsilon = 1e-10);
505
506        // Test Y₁⁰: Y₁⁰(θ, φ) = √(3/4π) cos(θ)
507        let factor_y10 = f64::sqrt(3.0 / (4.0 * PI));
508        let (re, im) = sph_harm_complex(1, 0, 0.0, 0.0).expect("Operation failed");
509        assert_relative_eq!(re, factor_y10, epsilon = 1e-10);
510        assert_relative_eq!(im, 0.0, epsilon = 1e-10);
511
512        // Test Y₁¹: Y₁¹(θ, φ) = -√(3/8π) sin(θ) e^(iφ)
513        let factor_y11 = -f64::sqrt(3.0 / (8.0 * PI));
514
515        // At (θ=π/2, φ=0): sin(θ)=1, e^(iφ)=1
516        let (re, im) = sph_harm_complex(1, 1, PI / 2.0, 0.0).expect("Operation failed");
517        assert_relative_eq!(re, factor_y11, epsilon = 1e-10);
518        assert_relative_eq!(im, 0.0, epsilon = 1e-10);
519
520        // At (θ=π/2, φ=π/2): sin(θ)=1, e^(iφ)=i
521        let (re, im) = sph_harm_complex(1, 1, PI / 2.0, PI / 2.0).expect("Operation failed");
522        assert_relative_eq!(re, 0.0, epsilon = 1e-10);
523        assert_relative_eq!(im, factor_y11, epsilon = 1e-10);
524
525        // Test Y₁⁻¹: Y₁⁻¹(θ, φ) = √(3/8π) sin(θ) e^(-iφ)
526        let factor_y1_neg1 = f64::sqrt(3.0 / (8.0 * PI));
527
528        // At (θ=π/2, φ=0): sin(θ)=1, e^(-iφ)=1
529        let (re, im) = sph_harm_complex(1, -1, PI / 2.0, 0.0).expect("Operation failed");
530        assert_relative_eq!(re, factor_y1_neg1, epsilon = 1e-10);
531        assert_relative_eq!(im, 0.0, epsilon = 1e-10);
532
533        // At (θ=π/2, φ=π/2): sin(θ)=1, e^(-iφ)=-i
534        let (re, im) = sph_harm_complex(1, -1, PI / 2.0, PI / 2.0).expect("Operation failed");
535        assert_relative_eq!(re, 0.0, epsilon = 1e-10);
536        assert_relative_eq!(im, -factor_y1_neg1, epsilon = 1e-10);
537    }
538
539    // ====== Additional spherical harmonic tests ======
540
541    #[test]
542    fn test_sph_harm_orthogonality_same_l() {
543        // For the same l, different m should give orthogonal functions
544        // Sum over theta grid should vanish for m1 != m2
545        // (Crude numerical test)
546        let n_theta = 50;
547        let n_phi = 50;
548        let d_theta = PI / (n_theta as f64);
549        let d_phi = 2.0 * PI / (n_phi as f64);
550
551        let mut integral = 0.0;
552        for i in 0..n_theta {
553            let theta = (i as f64 + 0.5) * d_theta;
554            for j in 0..n_phi {
555                let phi = (j as f64 + 0.5) * d_phi;
556                let y10: f64 = sph_harm(1, 0, theta, phi).expect("failed");
557                let y11: f64 = sph_harm(1, 1, theta, phi).expect("failed");
558                integral += y10 * y11 * theta.sin() * d_theta * d_phi;
559            }
560        }
561        assert!(
562            integral.abs() < 0.05,
563            "Y_1^0 and Y_1^1 should be orthogonal, integral = {integral}"
564        );
565    }
566
567    #[test]
568    fn test_sph_harm_normalization_function() {
569        let k00 = sph_harm_normalization(0, 0);
570        assert_relative_eq!(k00, 0.5 / PI.sqrt(), epsilon = 1e-14);
571
572        let k10 = sph_harm_normalization(1, 0);
573        let expected = (3.0 / (4.0 * PI)).sqrt();
574        assert_relative_eq!(k10, expected, epsilon = 1e-14);
575    }
576
577    #[test]
578    fn test_sph_harm_normalization_zero_for_invalid_m() {
579        assert_relative_eq!(sph_harm_normalization(1, 3), 0.0, epsilon = 1e-14);
580    }
581
582    #[test]
583    fn test_sph_harm_cos_angle_same_direction() {
584        let cos_g = sph_harm_cos_angle(PI / 4.0, PI / 3.0, PI / 4.0, PI / 3.0);
585        assert_relative_eq!(cos_g, 1.0, epsilon = 1e-12);
586    }
587
588    #[test]
589    fn test_sph_harm_cos_angle_opposite() {
590        let cos_g = sph_harm_cos_angle(0.0, 0.0, PI, 0.0);
591        assert_relative_eq!(cos_g, -1.0, epsilon = 1e-12);
592    }
593
594    #[test]
595    fn test_sph_harm_cos_angle_perpendicular() {
596        // z-axis and x-axis: cos(gamma) = 0
597        let cos_g = sph_harm_cos_angle(0.0, 0.0, PI / 2.0, 0.0);
598        assert_relative_eq!(cos_g, 0.0, epsilon = 1e-12);
599    }
600
601    // ====== Solid harmonic tests ======
602
603    #[test]
604    fn test_solid_harmonic_regular_l0() {
605        // R_0^0 = sqrt(4*pi) * 1/(2*sqrt(pi)) = 1
606        let r = solid_harmonic_regular(0, 0, 1.0, PI / 4.0, 0.0).expect("failed");
607        assert_relative_eq!(r, 1.0, epsilon = 1e-10);
608    }
609
610    #[test]
611    fn test_solid_harmonic_regular_scales_with_r() {
612        // R_l^m scales as r^l
613        let r1 = solid_harmonic_regular(2, 0, 1.0, PI / 4.0, 0.0).expect("failed");
614        let r2 = solid_harmonic_regular(2, 0, 2.0, PI / 4.0, 0.0).expect("failed");
615        assert_relative_eq!(r2, r1 * 4.0, epsilon = 1e-8);
616    }
617
618    #[test]
619    fn test_solid_harmonic_irregular_l0() {
620        // I_0^0(r=1) = sqrt(4pi) * 1/(2*sqrt(pi)) / r = 1
621        let r = solid_harmonic_irregular(0, 0, 1.0, PI / 4.0, 0.0).expect("failed");
622        assert_relative_eq!(r, 1.0, epsilon = 1e-10);
623    }
624
625    #[test]
626    fn test_solid_harmonic_irregular_zero_r_error() {
627        assert!(solid_harmonic_irregular(0, 0, 0.0, 0.0, 0.0).is_err());
628    }
629
630    #[test]
631    fn test_solid_harmonic_irregular_scales_with_r() {
632        // I_l^m scales as r^{-(l+1)}
633        let r1 = solid_harmonic_irregular(1, 0, 1.0, PI / 4.0, 0.0).expect("failed");
634        let r2 = solid_harmonic_irregular(1, 0, 2.0, PI / 4.0, 0.0).expect("failed");
635        // r^{-(l+1)} = r^{-2} for l=1, so ratio should be (1/2)^2 = 0.25
636        assert_relative_eq!(r2 / r1, 0.25, epsilon = 1e-8);
637    }
638
639    // ====== Addition theorem tests ======
640
641    #[test]
642    fn test_addition_theorem_l0() {
643        // For l=0, P_0(cos gamma) = 1, and the sum should also be 1
644        let (p_val, sum_val) =
645            sph_harm_addition_theorem_check(0, PI / 4.0, 0.0, PI / 3.0, PI / 6.0).expect("failed");
646        assert_relative_eq!(p_val, 1.0, epsilon = 1e-10);
647        assert_relative_eq!(sum_val, 1.0, epsilon = 1e-8);
648    }
649
650    #[test]
651    fn test_addition_theorem_same_direction() {
652        // For same direction, cos(gamma) = 1, P_l(1) = 1
653        let (p_val, sum_val) =
654            sph_harm_addition_theorem_check(2, PI / 4.0, PI / 3.0, PI / 4.0, PI / 3.0)
655                .expect("failed");
656        assert_relative_eq!(p_val, 1.0, epsilon = 1e-10);
657        assert_relative_eq!(sum_val, 1.0, epsilon = 1e-4);
658    }
659
660    // ====== Overflow regression tests (PR #119) ======
661
662    #[test]
663    fn test_sph_harm_pi_theta_finite() {
664        // All (l,m) pairs from the issue report should return finite values at theta=PI
665        let pairs = [
666            (1, 0),
667            (1, 1),
668            (2, 0),
669            (2, 1),
670            (2, 2),
671            (5, 3),
672            (10, 5),
673            (10, 10),
674            (20, 15),
675            (50, 50),
676            (100, 100),
677            (150, 150),
678        ];
679        for (l, m) in pairs {
680            let val: f64 = sph_harm(l, m, PI, 0.0).expect("sph_harm failed");
681            assert!(
682                val.is_finite(),
683                "sph_harm({l},{m},PI,0) = {val} is not finite"
684            );
685        }
686    }
687
688    #[test]
689    fn test_sph_harm_large_l_eq_m_finite() {
690        // l=m at theta=PI/2 — the old code would overflow for large l=m
691        let large_lm = [100, 130, 150, 151, 152, 200, 250, 500];
692        for l in large_lm {
693            let val: f64 = sph_harm(l, l as i32, PI / 2.0, 0.0).expect("sph_harm failed");
694            assert!(
695                val.is_finite(),
696                "sph_harm({l},{l},PI/2,0) = {val} is not finite"
697            );
698        }
699    }
700
701    #[test]
702    fn test_sph_harm_correctness_after_fix() {
703        // Spot-check analytical values after the overflow fix
704
705        // Y_0^0 = 1/(2*sqrt(PI))
706        let y00: f64 = sph_harm(0, 0, 1.0, 0.0).expect("failed");
707        assert_relative_eq!(y00, 0.5 / PI.sqrt(), epsilon = 1e-10);
708
709        // Y_1^0(theta, phi) = sqrt(3/(4*PI)) * cos(theta)
710        let theta = PI / 4.0;
711        let y10: f64 = sph_harm(1, 0, theta, 0.0).expect("failed");
712        let expected_y10 = (3.0 / (4.0 * PI)).sqrt() * theta.cos();
713        assert_relative_eq!(y10, expected_y10, epsilon = 1e-10);
714
715        // Y_1^1(theta, phi) = -sqrt(3/(4*PI)) * sin(theta) * cos(phi) (real form with sqrt2)
716        // Real convention: sqrt2 * K_1^1 * P_1^1(cos theta) * cos(phi)
717        // K_1^1 = sqrt(3/(8*PI)), P_1^1(x) = sin(theta) (no CS phase)
718        // => sqrt(2) * sqrt(3/(8*PI)) * sin(theta) * cos(phi)
719        let y11: f64 = sph_harm(1, 1, PI / 2.0, 0.0).expect("failed");
720        let expected_y11 = SQRT_2 * (3.0 / (8.0 * PI)).sqrt() * 1.0 * 1.0;
721        assert_relative_eq!(y11, expected_y11, epsilon = 1e-10);
722
723        // Y_2^0(theta=0) = sqrt(5/(16*PI)) * (3*1 - 1) = sqrt(5/(16*PI)) * 2
724        let y20: f64 = sph_harm(2, 0, 0.0, 0.0).expect("failed");
725        let expected_y20 = (5.0 / (16.0 * PI)).sqrt() * 2.0;
726        assert_relative_eq!(y20, expected_y20, epsilon = 1e-10);
727    }
728}