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}