use crate::error::{SpecialError, SpecialResult};
use crate::mathieu::advanced::{tridiag_eigenvalues, tridiag_eigenvector};
use std::f64::consts::PI;
#[derive(Debug, Clone)]
pub struct HillCoefficients {
pub theta: Vec<f64>,
pub period: f64,
}
impl HillCoefficients {
pub fn mathieu(q: f64) -> Self {
HillCoefficients {
theta: vec![0.0, q],
period: PI,
}
}
pub fn from_function<F: Fn(f64) -> f64>(f: F, period: f64, n_terms: usize) -> Self {
let m = 2 * n_terms + 1;
let dt = period / m as f64;
let samples: Vec<f64> = (0..m).map(|k| f(k as f64 * dt)).collect();
let mut theta = vec![0.0_f64; n_terms + 1];
theta[0] = samples.iter().sum::<f64>() / m as f64;
for k in 1..=n_terms {
let mut re = 0.0_f64;
for (j, &s) in samples.iter().enumerate() {
re += s * (2.0 * PI * k as f64 * j as f64 / m as f64).cos();
}
theta[k] = re / m as f64;
}
HillCoefficients { theta, period }
}
}
const N_HILL: usize = 10;
#[allow(dead_code)]
fn build_hill_matrix(coeffs: &HillCoefficients, lambda: f64) -> Vec<Vec<f64>> {
let n = N_HILL as i64;
let size = (2 * n + 1) as usize;
let omega = 2.0 * PI / coeffs.period;
let mut mat = vec![vec![0.0_f64; size]; size];
for i in 0..size {
let k = i as i64 - n;
for j in 0..size {
let l = j as i64 - n;
let diff = (k - l).unsigned_abs() as usize;
if i == j {
let q0 = coeffs.theta.first().copied().unwrap_or(0.0);
mat[i][j] = (k as f64 * omega).powi(2) - lambda + q0;
} else if diff < coeffs.theta.len() {
mat[i][j] = coeffs.theta[diff];
}
}
}
mat
}
#[allow(dead_code)]
fn matrix_det(mat: &[Vec<f64>]) -> f64 {
let n = mat.len();
if n == 0 {
return 1.0;
}
let mut a: Vec<Vec<f64>> = mat.to_vec();
let mut sign = 1.0_f64;
for col in 0..n {
let pivot_row = (col..n)
.max_by(|&r1, &r2| {
a[r1][col]
.abs()
.partial_cmp(&a[r2][col].abs())
.unwrap_or(std::cmp::Ordering::Equal)
})
.unwrap_or(col);
if a[pivot_row][col].abs() < 1e-300 {
return 0.0;
}
if pivot_row != col {
a.swap(pivot_row, col);
sign = -sign;
}
let diag_val = a[col][col];
for row in (col + 1)..n {
let factor = a[row][col] / diag_val;
for k in col..n {
let tmp = a[col][k];
a[row][k] -= factor * tmp;
}
}
}
let product: f64 = (0..n).map(|i| a[i][i]).product();
sign * product
}
fn build_hill_tridiag(coeffs: &HillCoefficients, lambda: f64) -> (Vec<f64>, Vec<f64>) {
let n = N_HILL as i64;
let size = (2 * n + 1) as usize;
let omega = 2.0 * PI / coeffs.period;
let q0 = coeffs.theta.first().copied().unwrap_or(0.0);
let q1 = coeffs.theta.get(1).copied().unwrap_or(0.0);
let diag: Vec<f64> = (0..size)
.map(|i| {
let k = i as i64 - n;
(k as f64 * omega).powi(2) - lambda + q0
})
.collect();
let off: Vec<f64> = (0..size.saturating_sub(1)).map(|_| q1).collect();
(diag, off)
}
pub fn hill_stability_exponent(coeffs: &HillCoefficients, lambda: f64) -> SpecialResult<f64> {
if coeffs.period <= 0.0 {
return Err(SpecialError::DomainError(format!(
"hill_stability_exponent: period={} must be positive",
coeffs.period
)));
}
let mu = hill_characteristic_exponent(coeffs, lambda)?;
Ok((PI * mu).cos())
}
pub fn hill_periodic_solution(
coeffs: &HillCoefficients,
lambda: f64,
x: &[f64],
) -> SpecialResult<Vec<f64>> {
if coeffs.period <= 0.0 {
return Err(SpecialError::DomainError(format!(
"hill_periodic_solution: period={} must be positive",
coeffs.period
)));
}
let omega = 2.0 * PI / coeffs.period;
let (diag, off) = build_hill_tridiag(coeffs, lambda);
let eigenvalues = tridiag_eigenvalues(&diag, &off);
if eigenvalues.is_empty() {
return Err(SpecialError::ComputationError(
"hill_periodic_solution: tridiagonal eigenvalue computation returned empty".to_string(),
));
}
let n_idx = N_HILL;
let center_diag = diag[n_idx];
let target_ev = eigenvalues
.iter()
.copied()
.min_by(|a, b| {
(a - center_diag)
.abs()
.partial_cmp(&(b - center_diag).abs())
.unwrap_or(std::cmp::Ordering::Equal)
})
.unwrap_or(center_diag);
let ck = tridiag_eigenvector(&diag, &off, target_ev);
let n = N_HILL as i64;
let result: Vec<f64> = x
.iter()
.map(|&t| {
let mut val = 0.0_f64;
for (i, &c) in ck.iter().enumerate() {
let k = i as i64 - n; val += c * (k as f64 * omega * t).cos();
}
val
})
.collect();
Ok(result)
}
pub fn hill_characteristic_exponent(coeffs: &HillCoefficients, lambda: f64) -> SpecialResult<f64> {
if coeffs.period <= 0.0 {
return Err(SpecialError::DomainError(format!(
"hill_characteristic_exponent: period={} must be positive",
coeffs.period
)));
}
let (diag, off) = build_hill_tridiag(coeffs, lambda);
let eigenvalues = tridiag_eigenvalues(&diag, &off);
if eigenvalues.is_empty() {
return Err(SpecialError::ComputationError(
"hill_characteristic_exponent: empty eigenvalue spectrum".to_string(),
));
}
let center_diag = diag[N_HILL];
let nearest_ev = eigenvalues
.iter()
.copied()
.min_by(|a, b| {
(a - center_diag)
.abs()
.partial_cmp(&(b - center_diag).abs())
.unwrap_or(std::cmp::Ordering::Equal)
})
.unwrap_or(center_diag);
let a_effective = lambda - (nearest_ev - center_diag);
let cos_mu_t = if a_effective >= 0.0 {
(coeffs.period * a_effective.sqrt()).cos()
} else {
(coeffs.period * (-a_effective).sqrt()).cosh()
};
let mu = if cos_mu_t.abs() <= 1.0 + 1e-10 {
cos_mu_t.clamp(-1.0, 1.0).acos() / coeffs.period
} else {
let c = cos_mu_t.abs();
(c + (c * c - 1.0).max(0.0).sqrt()).ln() / coeffs.period
};
Ok(mu)
}
pub fn hill_stability_check(coeffs: &HillCoefficients, lambda: f64) -> SpecialResult<bool> {
if coeffs.period <= 0.0 {
return Err(SpecialError::DomainError(format!(
"hill_stability_check: period={} must be positive",
coeffs.period
)));
}
let cos_mu_t = hill_stability_exponent(coeffs, lambda)?;
Ok(cos_mu_t.abs() <= 1.0 + 1e-8)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mathieu_stable_a1_q05() {
let coeffs = HillCoefficients::mathieu(0.5);
let stable = hill_stability_check(&coeffs, 1.0).expect("stability check should not error");
assert!(stable, "Mathieu a=1, q=0.5 should be stable");
}
#[test]
fn test_trivial_stable_q0() {
let coeffs = HillCoefficients {
theta: vec![0.0],
period: PI,
};
let stable = hill_stability_check(&coeffs, 1.0)
.expect("trivial q=0 stability check should not error");
assert!(
stable,
"Hill with q=0, λ=1 should be stable (pure oscillation)"
);
}
#[test]
fn test_characteristic_exponent_real_stable() {
let coeffs = HillCoefficients::mathieu(0.1);
let mu = hill_characteristic_exponent(&coeffs, 1.0)
.expect("characteristic exponent should not error");
assert!(
mu.is_finite(),
"characteristic exponent should be finite, got {mu}"
);
assert!(
mu >= 0.0,
"characteristic exponent should be non-negative, got {mu}"
);
}
#[test]
fn test_periodic_solution_has_correct_length() {
let coeffs = HillCoefficients::mathieu(0.2);
let pts: Vec<f64> = (0..=20).map(|i| i as f64 * PI / 20.0).collect();
let y =
hill_periodic_solution(&coeffs, 1.0, &pts).expect("periodic solution should not error");
assert_eq!(
y.len(),
pts.len(),
"output length should match input length"
);
}
#[test]
fn test_periodic_solution_finite() {
let coeffs = HillCoefficients::mathieu(0.3);
let pts: Vec<f64> = (0..=15).map(|i| i as f64 * PI / 15.0).collect();
let y =
hill_periodic_solution(&coeffs, 1.0, &pts).expect("periodic solution should not error");
for (i, v) in y.iter().enumerate() {
assert!(v.is_finite(), "y[{i}] = {v} should be finite");
}
}
#[test]
fn test_stability_exponent_in_range_stable() {
let coeffs = HillCoefficients::mathieu(0.5);
let s = hill_stability_exponent(&coeffs, 1.0).expect("stability exponent should not error");
assert!(
s.abs() <= 1.0 + 1e-6,
"stability exponent for stable Mathieu should be in [-1,1], got {s}"
);
}
#[test]
fn test_mathieu_q0_cos_mu_t_equals_cos_sqrt_a() {
let coeffs = HillCoefficients {
theta: vec![0.0],
period: PI,
};
let s =
hill_stability_exponent(&coeffs, 1.0).expect("stability exponent q=0 should not error");
assert!(
s.is_finite(),
"stability exponent q=0 λ=1 should be finite, got {s}"
);
assert!(
(s + 1.0).abs() < 0.5,
"q=0 λ=1 T=π: cos(πμ) should be near -1 (boundary), got {s}"
);
}
#[test]
fn test_hill_from_function_mathieu_coefficients() {
let q = 0.4;
let coeffs = HillCoefficients::from_function(|t| 2.0 * q * (2.0 * t).cos(), PI, 3);
assert!(
(coeffs.theta[1] - q).abs() < 1e-8,
"θ_1 should recover q={q}, got {}",
coeffs.theta[1]
);
assert!(
coeffs.theta[0].abs() < 1e-10,
"θ_0 of pure cosine should be ≈ 0, got {}",
coeffs.theta[0]
);
}
#[test]
fn test_hill_stability_transitions() {
let q = 0.1_f64;
let coeffs = HillCoefficients::mathieu(q);
let stable_a = hill_stability_check(&coeffs, 0.5)
.expect("stability check a=0.5 q=0.1 should not error");
assert!(stable_a, "Mathieu a=0.5 q=0.1 should be stable");
}
#[test]
fn test_hill_coefficients_mathieu_constructor() {
let coeffs = HillCoefficients::mathieu(0.3);
assert_eq!(coeffs.theta.len(), 2);
assert!(coeffs.theta[0].abs() < 1e-14, "DC term should be 0");
assert!((coeffs.theta[1] - 0.3).abs() < 1e-14, "θ_1 should be q=0.3");
assert!((coeffs.period - PI).abs() < 1e-14, "period should be π");
}
#[test]
fn test_periodic_solution_nonzero() {
let coeffs = HillCoefficients::mathieu(0.5);
let pts = vec![0.0, 0.5, 1.0, 1.5, 2.0];
let y =
hill_periodic_solution(&coeffs, 1.0, &pts).expect("periodic solution should not error");
let norm_sq: f64 = y.iter().map(|v| v * v).sum();
assert!(
norm_sq > 1e-20,
"periodic solution should not be identically zero"
);
}
#[test]
fn test_characteristic_exponent_positive() {
let coeffs = HillCoefficients::mathieu(0.4);
let mu = hill_characteristic_exponent(&coeffs, 1.0).expect("exponent should not error");
assert!(
mu >= 0.0,
"characteristic exponent should be >= 0, got {mu}"
);
}
}