use super::types::{EllipsoidalCoord, LameConfig, LameResult, LameSpecies};
use super::{cn, dn, elliptic_k, lame_eigenvalue, lame_function, sn, LameClass, LameOrder};
use crate::error::{SpecialError, SpecialResult};
#[derive(Debug, Clone)]
pub struct LameFunction {
pub h2: f64,
pub k2: f64,
pub degree: usize,
pub species: usize,
pub config: LameConfig,
eigenvalue: Option<f64>,
coefficients: Option<Vec<f64>>,
}
impl LameFunction {
pub fn new(
h2: f64,
k2: f64,
degree: usize,
species: usize,
config: LameConfig,
) -> SpecialResult<Self> {
if h2 < 0.0 {
return Err(SpecialError::DomainError(
"h² must be non-negative".to_string(),
));
}
if k2 < h2 {
return Err(SpecialError::DomainError("k² must be >= h²".to_string()));
}
if degree > config.max_degree {
return Err(SpecialError::DomainError(format!(
"degree {degree} exceeds max_degree {}",
config.max_degree
)));
}
if species == 0 || species > 2 * degree + 1 {
return Err(SpecialError::DomainError(format!(
"species m must be in [1, {}] for degree {degree}, got {species}",
2 * degree + 1
)));
}
Ok(LameFunction {
h2,
k2,
degree,
species,
config,
eigenvalue: None,
coefficients: None,
})
}
pub fn compute_eigenvalue(&mut self) -> SpecialResult<f64> {
if let Some(h) = self.eigenvalue {
return Ok(h);
}
let k = self.elliptic_modulus()?;
let (class, class_index) = self.species_to_class_index();
let order = LameOrder::new(self.degree, class_index)
.map_err(|e| SpecialError::ComputationError(format!("{e}")))?;
let lame_config = super::LameConfig {
n_fourier: self.config.n_fourier,
tol: self.config.tol,
};
let h = lame_eigenvalue(&order, k, &class, &lame_config)?;
self.eigenvalue = Some(h);
Ok(h)
}
pub fn evaluate(&mut self, s: f64) -> SpecialResult<LameResult> {
let k = self.elliptic_modulus()?;
let (class, class_index) = self.species_to_class_index();
let order = LameOrder::new(self.degree, class_index)
.map_err(|e| SpecialError::ComputationError(format!("{e}")))?;
let lame_config = super::LameConfig {
n_fourier: self.config.n_fourier,
tol: self.config.tol,
};
let eigenvalue = match self.eigenvalue {
Some(h) => h,
None => {
let h = lame_eigenvalue(&order, k, &class, &lame_config)?;
self.eigenvalue = Some(h);
h
}
};
let value = if self.config.use_small_k_expansion && k * k < 0.01 {
self.evaluate_small_k(s, k)?
} else {
self.evaluate_fourier(s, k, &order, &lame_config)?
};
Ok(LameResult {
value,
eigenvalue,
degree: self.degree,
species: self.species,
error_estimate: None,
})
}
fn evaluate_fourier(
&self,
s: f64,
k: f64,
order: &LameOrder,
config: &super::LameConfig,
) -> SpecialResult<f64> {
let u = self.s_to_u(s, k)?;
lame_function(order, k, u, config)
}
fn evaluate_small_k(&self, s: f64, k: f64) -> SpecialResult<f64> {
let n = self.degree;
let k2 = k * k;
let cos_theta = if (0.0..=1.0).contains(&s) {
s.sqrt()
} else if s > 1.0 {
1.0 } else {
0.0
};
let p_n = legendre_p(n, cos_theta);
let correction = k2 * self.first_order_k2_correction(cos_theta)?;
Ok(p_n + correction)
}
fn first_order_k2_correction(&self, x: f64) -> SpecialResult<f64> {
let n = self.degree;
let m = self.species;
if n < 2 {
return Ok(0.0);
}
let p_np2 = legendre_p(n + 2, x);
let p_nm2 = if n >= 2 { legendre_p(n - 2, x) } else { 0.0 };
let factor = (n as f64) * (n as f64 + 1.0) / (2.0 * (2.0 * n as f64 + 1.0));
let correction = factor * (p_np2 / (2.0 * n as f64 + 3.0) - p_nm2 / (2.0 * n as f64 - 1.0));
let phase = if m.is_multiple_of(2) { 1.0 } else { -1.0 };
Ok(correction * phase)
}
fn s_to_u(&self, s: f64, k: f64) -> SpecialResult<f64> {
if s.abs() < 1e-15 {
return Ok(0.0);
}
let big_k = elliptic_k(k)?;
if (0.0..=1.0).contains(&s) {
let target = s.sqrt();
let mut u = target.asin();
for _ in 0..20 {
let sn_val = sn(u, k)?;
let cn_val = cn(u, k)?;
let dn_val = dn(u, k)?;
let residual = sn_val - target;
if residual.abs() < 1e-14 {
break;
}
let deriv = cn_val * dn_val;
if deriv.abs() < 1e-15 {
break;
}
u -= residual / deriv;
}
u = u.clamp(0.0, big_k);
Ok(u)
} else if s > 1.0 {
Ok(big_k * s.sqrt().min(10.0))
} else {
Ok((-s).sqrt().asin().min(big_k))
}
}
fn species_to_class_index(&self) -> (LameClass, usize) {
let n = self.degree;
let m = self.species;
let species_list = LameSpecies::all_for_degree(n);
if m == 0 || m > species_list.len() {
return (LameClass::K, 0);
}
match &species_list[m - 1] {
LameSpecies::K(i) => (LameClass::K, *i),
LameSpecies::L(i) => (LameClass::L, *i),
LameSpecies::M(i) => (LameClass::M, *i),
LameSpecies::N(i) => (LameClass::N, *i),
}
}
fn elliptic_modulus(&self) -> SpecialResult<f64> {
if self.k2 <= 0.0 {
return Ok(0.0);
}
let k = (self.h2 / self.k2).sqrt();
if k >= 1.0 {
return Err(SpecialError::DomainError(
"Elliptic modulus must be < 1".to_string(),
));
}
Ok(k)
}
}
pub fn interior_harmonic(
coord: &EllipsoidalCoord,
h2: f64,
k2: f64,
degree: usize,
species: usize,
config: &LameConfig,
) -> SpecialResult<f64> {
let mut lf = LameFunction::new(h2, k2, degree, species, config.clone())?;
let e_rho = lf.evaluate(coord.rho * coord.rho)?;
let e_mu = lf.evaluate(coord.mu * coord.mu)?;
let e_nu = lf.evaluate(coord.nu * coord.nu)?;
Ok(e_rho.value * e_mu.value * e_nu.value)
}
pub fn exterior_harmonic(
coord: &EllipsoidalCoord,
h2: f64,
k2: f64,
degree: usize,
species: usize,
config: &LameConfig,
) -> SpecialResult<f64> {
let s_val = interior_harmonic(coord, h2, k2, degree, species, config)?;
let i_nm = exterior_integral(coord.rho, h2, k2, degree, species, config)?;
let factor = (2 * degree + 1) as f64;
Ok(factor * s_val * i_nm)
}
fn exterior_integral(
rho: f64,
h2: f64,
k2: f64,
degree: usize,
species: usize,
config: &LameConfig,
) -> SpecialResult<f64> {
let n_points = 32;
let mut integral = 0.0;
let mut lf = LameFunction::new(h2, k2, degree, species, config.clone())?;
for i in 0..n_points {
let u = (i as f64 + 0.5) / n_points as f64; if u < 1e-15 {
continue;
}
let t = rho / u;
let t2 = t * t;
let e_val = lf.evaluate(t2)?;
let e_sq = e_val.value * e_val.value;
if e_sq.abs() < 1e-30 {
continue;
}
let r_val = t2 * (t2 - h2) * (t2 - k2);
if r_val <= 0.0 {
continue;
}
let sqrt_r = r_val.sqrt();
let jacobian = rho / (u * u);
integral += jacobian / (e_sq * sqrt_r) / n_points as f64;
}
Ok(integral)
}
pub fn bocher_eigenvalue(
h2: f64,
k2: f64,
degree: usize,
species: usize,
config: &LameConfig,
) -> SpecialResult<f64> {
let mut lf = LameFunction::new(h2, k2, degree, species, config.clone())?;
lf.compute_eigenvalue()
}
pub fn lame_function_array(
h2: f64,
k2: f64,
degree: usize,
species: usize,
points: &[f64],
config: &LameConfig,
) -> SpecialResult<Vec<LameResult>> {
let mut lf = LameFunction::new(h2, k2, degree, species, config.clone())?;
let mut results = Vec::with_capacity(points.len());
for &s in points {
results.push(lf.evaluate(s)?);
}
Ok(results)
}
pub fn check_symmetry(
h2: f64,
k2: f64,
degree: usize,
species: usize,
s: f64,
config: &LameConfig,
) -> SpecialResult<(f64, f64, f64)> {
let mut lf = LameFunction::new(h2, k2, degree, species, config.clone())?;
let e_pos = lf.evaluate(s)?;
let e_neg = lf.evaluate(-s)?;
let species_list = LameSpecies::all_for_degree(degree);
let parity = if species > 0 && species <= species_list.len() {
match &species_list[species - 1] {
LameSpecies::K(_) | LameSpecies::M(_) => 1.0, LameSpecies::L(_) | LameSpecies::N(_) => -1.0, }
} else {
1.0
};
Ok((e_pos.value, e_neg.value, parity))
}
fn legendre_p(n: usize, x: f64) -> f64 {
match n {
0 => 1.0,
1 => x,
_ => {
let mut p_prev = 1.0;
let mut p_curr = x;
for k in 1..n {
let p_next = ((2 * k + 1) as f64 * x * p_curr - k as f64 * p_prev) / (k + 1) as f64;
p_prev = p_curr;
p_curr = p_next;
}
p_curr
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lame_function_creation() {
let config = LameConfig::default();
let lf = LameFunction::new(0.5, 1.0, 2, 1, config);
assert!(lf.is_ok());
}
#[test]
fn test_lame_function_invalid_params() {
let config = LameConfig::default();
assert!(LameFunction::new(-0.1, 1.0, 2, 1, config.clone()).is_err());
assert!(LameFunction::new(1.0, 0.5, 2, 1, config.clone()).is_err());
assert!(LameFunction::new(0.5, 1.0, 2, 0, config.clone()).is_err());
assert!(LameFunction::new(0.5, 1.0, 2, 6, config).is_err());
}
#[test]
fn test_eigenvalue_computation() {
let config = LameConfig::default();
let mut lf = LameFunction::new(0.3, 1.0, 2, 1, config).expect("creation failed");
let h = lf.compute_eigenvalue();
assert!(h.is_ok(), "eigenvalue computation failed: {:?}", h.err());
let h_val = h.expect("eigenvalue failed");
assert!(h_val.is_finite(), "eigenvalue not finite: {h_val}");
}
#[test]
fn test_evaluate_degree_zero() {
let config = LameConfig::default();
let mut lf = LameFunction::new(0.3, 1.0, 0, 1, config).expect("creation failed");
let r1 = lf.evaluate(0.2).expect("eval failed");
let r2 = lf.evaluate(0.5).expect("eval failed");
assert!(r1.value.is_finite());
assert!(r2.value.is_finite());
}
#[test]
fn test_evaluate_small_k() {
let config = LameConfig {
use_small_k_expansion: true,
..LameConfig::default()
};
let mut lf = LameFunction::new(0.001, 1.0, 2, 1, config).expect("creation failed");
let result = lf.evaluate(0.5).expect("eval failed");
assert!(result.value.is_finite());
}
#[test]
fn test_interior_harmonic() {
let config = LameConfig::default();
let coord = EllipsoidalCoord {
rho: 2.0,
mu: 1.5,
nu: 0.5,
};
let result = interior_harmonic(&coord, 0.3, 1.0, 1, 1, &config);
assert!(result.is_ok());
assert!(result.expect("interior harmonic failed").is_finite());
}
#[test]
fn test_bocher_eigenvalue() {
let config = LameConfig::default();
for n in 0..=4 {
for m in 1..=(2 * n + 1) {
let result = bocher_eigenvalue(0.3, 1.0, n, m, &config);
assert!(
result.is_ok(),
"bocher_eigenvalue failed for n={n}, m={m}: {:?}",
result.err()
);
let h = result.expect("eigenvalue failed");
assert!(h.is_finite(), "eigenvalue not finite for n={n}, m={m}: {h}");
}
}
}
#[test]
fn test_eigenvalue_pairs_low_order() {
let config = LameConfig::default();
let h0 = bocher_eigenvalue(0.3, 1.0, 0, 1, &config).expect("failed");
assert!(h0.abs() < 5.0, "Degree 0 eigenvalue too large: {h0}");
let h1_1 = bocher_eigenvalue(0.3, 1.0, 1, 1, &config).expect("failed");
let h1_2 = bocher_eigenvalue(0.3, 1.0, 1, 2, &config).expect("failed");
let h1_3 = bocher_eigenvalue(0.3, 1.0, 1, 3, &config).expect("failed");
assert!(h1_1.is_finite());
assert!(h1_2.is_finite());
assert!(h1_3.is_finite());
}
#[test]
fn test_spherical_limit() {
let config = LameConfig {
use_small_k_expansion: true,
..LameConfig::default()
};
let eps = 0.001;
let mut lf = LameFunction::new(eps * 0.5, eps, 2, 1, config).expect("creation failed");
let result = lf.evaluate(0.5).expect("eval failed");
let p2 = legendre_p(2, 0.5_f64.sqrt());
assert!(
(result.value - p2).abs() < 2.0,
"Spherical limit: got {}, expected P_2(0.707) = {p2}",
result.value
);
}
#[test]
fn test_lame_function_array_eval() {
let config = LameConfig::default();
let points = vec![0.1, 0.3, 0.5, 0.7, 0.9];
let results = lame_function_array(0.3, 1.0, 2, 1, &points, &config);
assert!(results.is_ok());
let results = results.expect("array eval failed");
assert_eq!(results.len(), 5);
for r in &results {
assert!(r.value.is_finite());
}
}
#[test]
fn test_legendre_p_known_values() {
assert!((legendre_p(0, 0.5) - 1.0).abs() < 1e-14);
assert!((legendre_p(1, 0.5) - 0.5).abs() < 1e-14);
assert!((legendre_p(2, 0.5) - (-0.125)).abs() < 1e-14);
assert!((legendre_p(3, 0.5) - (-0.4375)).abs() < 1e-14);
}
}