pub mod ellipsoidal;
pub mod types;
pub use ellipsoidal::{
bocher_eigenvalue, check_symmetry, exterior_harmonic, interior_harmonic, lame_function_array,
LameFunction as LameFunctionExt,
};
pub use types::{EllipsoidalCoord, LameConfig as LameConfigExt, LameResult, LameSpecies};
use std::f64::consts::PI;
use crate::error::{SpecialError, SpecialResult};
use crate::mathieu::advanced::{tridiag_eigenvalues, tridiag_eigenvector};
#[derive(Debug, Clone)]
pub struct LameConfig {
pub n_fourier: usize,
pub tol: f64,
}
impl Default for LameConfig {
fn default() -> Self {
LameConfig {
n_fourier: 32,
tol: 1e-12,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LameOrder {
pub degree: usize,
pub order: usize,
}
impl LameOrder {
pub fn new(degree: usize, order: usize) -> SpecialResult<Self> {
if order > degree {
return Err(SpecialError::DomainError(format!(
"Lamé order m={order} must satisfy m ≤ n={degree}"
)));
}
Ok(LameOrder { degree, order })
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LameClass {
K,
KKprime,
L,
M,
N,
}
pub fn elliptic_k(k: f64) -> SpecialResult<f64> {
if !(0.0..1.0).contains(&k) {
return Err(SpecialError::DomainError(format!(
"elliptic_k requires 0 ≤ k < 1, got k = {k}"
)));
}
if k == 0.0 {
return Ok(PI / 2.0);
}
let mut a = 1.0_f64;
let mut b = (1.0 - k * k).sqrt();
for _ in 0..64 {
let a_new = (a + b) / 2.0;
let b_new = (a * b).sqrt();
if (a_new - b_new).abs() < 1e-15 * a_new {
return Ok(PI / (2.0 * a_new));
}
a = a_new;
b = b_new;
}
Ok(PI / (2.0 * a))
}
fn agm_amplitude(u: f64, k: f64) -> (f64, f64) {
let max_levels = 32usize;
let mut a_arr = vec![0.0_f64; max_levels + 1];
let mut b_arr = vec![0.0_f64; max_levels + 1];
let mut c_arr = vec![0.0_f64; max_levels + 1];
a_arr[0] = 1.0;
b_arr[0] = (1.0 - k * k).sqrt();
c_arr[0] = k;
let mut n = 0usize;
for i in 0..max_levels {
a_arr[i + 1] = (a_arr[i] + b_arr[i]) / 2.0;
b_arr[i + 1] = (a_arr[i] * b_arr[i]).sqrt();
c_arr[i + 1] = (a_arr[i] - b_arr[i]) / 2.0;
n = i + 1;
if c_arr[i + 1].abs() < 1e-15 * a_arr[i + 1] {
break;
}
}
let mut phi = (2.0_f64.powi(n as i32)) * a_arr[n] * u;
for i in (1..=n).rev() {
phi = (phi + (c_arr[i] / a_arr[i] * phi.sin()).asin()) / 2.0;
}
(phi, a_arr[n])
}
pub fn sn(u: f64, k: f64) -> SpecialResult<f64> {
if !(0.0..1.0).contains(&k) {
return Err(SpecialError::DomainError(format!(
"sn(u,k) requires 0 ≤ k < 1, got k = {k}"
)));
}
if k == 0.0 {
return Ok(u.sin());
}
let (phi, _) = agm_amplitude(u, k);
Ok(phi.sin())
}
pub fn cn(u: f64, k: f64) -> SpecialResult<f64> {
if !(0.0..1.0).contains(&k) {
return Err(SpecialError::DomainError(format!(
"cn(u,k) requires 0 ≤ k < 1, got k = {k}"
)));
}
if k == 0.0 {
return Ok(u.cos());
}
let (phi, _) = agm_amplitude(u, k);
Ok(phi.cos())
}
pub fn dn(u: f64, k: f64) -> SpecialResult<f64> {
if !(0.0..1.0).contains(&k) {
return Err(SpecialError::DomainError(format!(
"dn(u,k) requires 0 ≤ k < 1, got k = {k}"
)));
}
if k == 0.0 {
return Ok(1.0);
}
let sn_val = sn(u, k)?;
Ok((1.0 - k * k * sn_val * sn_val).sqrt())
}
fn lame_tridiag_k(n: usize, k: f64, config: &LameConfig) -> (Vec<f64>, Vec<f64>) {
let k2 = k * k;
let nf = config.n_fourier;
let mut diag = vec![0.0_f64; nf];
let mut off = vec![0.0_f64; nf - 1];
if n.is_multiple_of(2) {
let q = (n as f64) * (n as f64 + 1.0) * k2 / 4.0;
diag[0] = 0.0;
if nf > 1 {
off[0] = 2.0_f64.sqrt() * q;
}
for p in 1..nf {
let r = 2.0 * p as f64;
diag[p] = r * r;
if p < nf - 1 {
off[p] = q;
}
}
} else {
let q = (n as f64) * (n as f64 + 1.0) * k2 / 4.0;
for p in 0..nf {
let r = (2 * p + 1) as f64;
diag[p] = r * r;
if p < nf - 1 {
off[p] = q;
}
}
}
(diag, off)
}
pub fn lame_eigenvalue(
order: &LameOrder,
k: f64,
lame_class: &LameClass,
config: &LameConfig,
) -> SpecialResult<f64> {
if !(0.0..1.0).contains(&k) {
return Err(SpecialError::DomainError(format!(
"lame_eigenvalue requires 0 < k < 1, got k = {k}"
)));
}
let n = order.degree;
let m = order.order;
match lame_class {
LameClass::K => {
let (diag, off) = lame_tridiag_k(n, k, config);
let eigenvalues = tridiag_eigenvalues(&diag, &off);
let k2 = k * k;
let q_shift = (n as f64) * (n as f64 + 1.0) * k2 / 2.0;
let idx = m / 2;
if idx >= eigenvalues.len() {
return Err(SpecialError::ComputationError(format!(
"No eigenvalue found for degree={n}, order={m}, class K"
)));
}
Ok(eigenvalues[idx] + q_shift)
}
LameClass::L => {
let k2 = k * k;
let q = (n as f64) * (n as f64 + 1.0) * k2 / 4.0;
let nf = config.n_fourier;
let mut diag = vec![0.0_f64; nf];
let mut off = vec![0.0_f64; nf.saturating_sub(1)];
for p in 0..nf {
let r = (2 * p + 1) as f64;
diag[p] = r * r;
if p + 1 < nf {
off[p] = q;
}
}
let eigenvalues = tridiag_eigenvalues(&diag, &off);
let q_shift = (n as f64) * (n as f64 + 1.0) * k2 / 2.0;
let idx = m / 2;
if idx >= eigenvalues.len() {
return Err(SpecialError::ComputationError(format!(
"No eigenvalue found for degree={n}, order={m}, class L"
)));
}
Ok(eigenvalues[idx] + q_shift)
}
LameClass::M => {
let k2 = k * k;
let q = (n as f64) * (n as f64 + 1.0) * k2 / 4.0;
let nf = config.n_fourier;
let mut diag = vec![0.0_f64; nf];
let mut off = vec![0.0_f64; nf.saturating_sub(1)];
for p in 0..nf {
let r = 2.0 * (p + 1) as f64;
diag[p] = r * r;
if p + 1 < nf {
off[p] = q;
}
}
let eigenvalues = tridiag_eigenvalues(&diag, &off);
let q_shift = (n as f64) * (n as f64 + 1.0) * k2 / 2.0;
let idx = if m == 0 { 0 } else { (m - 1) / 2 };
if idx >= eigenvalues.len() {
return Err(SpecialError::ComputationError(format!(
"No eigenvalue found for degree={n}, order={m}, class M"
)));
}
Ok(eigenvalues[idx] + q_shift)
}
LameClass::KKprime | LameClass::N => {
lame_eigenvalue(order, k, &LameClass::K, config)
}
}
}
pub fn lame_function(order: &LameOrder, k: f64, u: f64, config: &LameConfig) -> SpecialResult<f64> {
if !(0.0..1.0).contains(&k) {
return Err(SpecialError::DomainError(format!(
"lame_function requires 0 < k < 1, got k = {k}"
)));
}
let n = order.degree;
let m = order.order;
let (diag, off) = lame_tridiag_k(n, k, config);
let eigenvalues = tridiag_eigenvalues(&diag, &off);
let idx = m / 2;
if idx >= eigenvalues.len() {
return Err(SpecialError::ComputationError(format!(
"No eigenvalue found for degree={n}, order={m}"
)));
}
let eigenval = eigenvalues[idx];
let coeffs = tridiag_eigenvector(&diag, &off, eigenval);
let (phi, _) = agm_amplitude(u, k);
let mut result = 0.0_f64;
if n.is_multiple_of(2) {
result += coeffs[0] * 2.0_f64.sqrt();
for (p, coeff) in coeffs.iter().enumerate().skip(1) {
result += coeff * (2.0 * p as f64 * phi).cos();
}
} else {
for (p, coeff) in coeffs.iter().enumerate() {
result += coeff * ((2.0 * p as f64 + 1.0) * phi).cos();
}
}
Ok(result)
}
pub fn lame_normalization(order: &LameOrder, k: f64, config: &LameConfig) -> SpecialResult<f64> {
if !(0.0..1.0).contains(&k) {
return Err(SpecialError::DomainError(format!(
"lame_normalization requires 0 < k < 1, got k = {k}"
)));
}
let big_k = elliptic_k(k)?;
let (nodes, weights) = gauss_legendre_64();
let half_k = big_k / 2.0;
let integral: f64 = nodes
.iter()
.zip(weights.iter())
.map(|(&xi, &wi)| {
let u = half_k * (xi + 1.0); let fu = lame_function(order, k, u, config).unwrap_or(0.0);
wi * fu * fu
})
.sum();
Ok(integral * half_k)
}
fn gauss_legendre_64() -> (Vec<f64>, Vec<f64>) {
let nodes = vec![
-0.9894009349916499,
-0.9445750230732326,
-0.8656312023341521,
-0.7554044083550030,
-0.6178762444026438,
-0.4580167776572274,
-0.2816035507792589,
-0.0950125098360223,
0.0950125098360223,
0.2816035507792589,
0.4580167776572274,
0.6178762444026438,
0.7554044083550030,
0.8656312023341521,
0.9445750230732326,
0.9894009349916499,
];
let weights = vec![
0.0271524594117541,
0.0622535239386479,
0.0951585116824928,
0.1246289512509060,
0.1495959888165767,
0.1691565193950025,
0.1826034150449236,
0.1894506104550685,
0.1894506104550685,
0.1826034150449236,
0.1691565193950025,
0.1495959888165767,
0.1246289512509060,
0.0951585116824928,
0.0622535239386479,
0.0271524594117541,
];
(nodes, weights)
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::PI;
#[test]
fn test_elliptic_k_zero() {
let k_val = elliptic_k(0.0).unwrap();
assert!(
(k_val - PI / 2.0).abs() < 1e-12,
"K(0) should be π/2, got {k_val}"
);
}
#[test]
fn test_elliptic_k_half() {
let k_val = elliptic_k(0.5).unwrap();
assert!(
(k_val - 1.6857503548).abs() < 1e-7,
"K(0.5) ≈ 1.6857503548, got {k_val}"
);
}
#[test]
fn test_jacobi_identity_cn_sq_plus_sn_sq() {
let k = 0.7;
for &u in &[0.1, 0.5, 1.0, 1.5] {
let s = sn(u, k).unwrap();
let c = cn(u, k).unwrap();
let sum = s * s + c * c;
assert!(
(sum - 1.0).abs() < 1e-12,
"cn²+sn²=1 failed at u={u}: got {sum}"
);
}
}
#[test]
fn test_jacobi_identity_dn() {
let k = 0.7;
for &u in &[0.1, 0.5, 1.0, 1.5] {
let s = sn(u, k).unwrap();
let d = dn(u, k).unwrap();
let sum = d * d + k * k * s * s;
assert!(
(sum - 1.0).abs() < 1e-12,
"dn²+k²sn²=1 failed at u={u}: got {sum}"
);
}
}
#[test]
fn test_sn_at_quarter_period() {
let k = 0.5;
let big_k = elliptic_k(k).unwrap();
let sn_val = sn(big_k, k).unwrap();
assert!(
(sn_val - 1.0).abs() < 1e-10,
"sn(K(k), k) should be 1, got {sn_val}"
);
}
#[test]
fn test_sn_zero() {
let k = 0.5;
let val = sn(0.0, k).unwrap();
assert!(val.abs() < 1e-14, "sn(0,k) should be 0, got {val}");
}
#[test]
fn test_cn_zero() {
let k = 0.5;
let val = cn(0.0, k).unwrap();
assert!((val - 1.0).abs() < 1e-14, "cn(0,k) should be 1, got {val}");
}
#[test]
fn test_lame_order_validation() {
assert!(LameOrder::new(3, 4).is_err());
assert!(LameOrder::new(3, 3).is_ok());
assert!(LameOrder::new(3, 0).is_ok());
}
#[test]
fn test_lame_eigenvalue_k0() {
let k = 0.01;
let config = LameConfig::default();
let order = LameOrder::new(4, 0).unwrap();
let h = lame_eigenvalue(&order, k, &LameClass::K, &config).unwrap();
assert!(h.is_finite(), "lame_eigenvalue should be finite, got {h}");
}
#[test]
fn test_lame_eigenvalue_k_limit() {
let k = 0.001;
let config = LameConfig::default();
let order = LameOrder::new(2, 0).unwrap();
let h = lame_eigenvalue(&order, k, &LameClass::K, &config).unwrap();
assert!(h.abs() < 0.1, "For small k, first eigenvalue ≈ 0, got {h}");
}
#[test]
fn test_lame_function_finite() {
let k = 0.5;
let config = LameConfig::default();
let order = LameOrder::new(2, 0).unwrap();
let val = lame_function(&order, k, 0.5, &config).unwrap();
assert!(
val.is_finite(),
"lame_function should return finite value, got {val}"
);
}
#[test]
fn test_lame_normalization_positive() {
let k = 0.5;
let config = LameConfig::default();
let order = LameOrder::new(2, 0).unwrap();
let norm = lame_normalization(&order, k, &config).unwrap();
assert!(
norm > 0.0,
"Normalization integral should be positive, got {norm}"
);
}
}