use std::f64::consts::PI;
const INV_SQRT_PI: f64 = 0.5641895835477563;
const AI_C1: f64 = 0.3550280538878172; const AI_C2: f64 = 0.2588194037928068;
const BI_C1: f64 = 0.6149266274460007; const BI_C2: f64 = 0.4482883573538264;
const MAX_SERIES_TERMS: usize = 100;
const EPSILON: f64 = 1e-15;
const ASYMP_THRESHOLD: f64 = 10.0;
pub fn airy_ai_scalar(x: f64) -> f64 {
if x.is_nan() {
return f64::NAN;
}
if x > ASYMP_THRESHOLD {
airy_ai_asymp_pos(x)
} else if x < -ASYMP_THRESHOLD {
airy_ai_asymp_neg(x)
} else {
airy_ai_series(x)
}
}
pub fn airy_bi_scalar(x: f64) -> f64 {
if x.is_nan() {
return f64::NAN;
}
if x > ASYMP_THRESHOLD {
airy_bi_asymp_pos(x)
} else if x < -ASYMP_THRESHOLD {
airy_bi_asymp_neg(x)
} else {
airy_bi_series(x)
}
}
fn airy_ai_series(x: f64) -> f64 {
if x.abs() < 1e-10 {
return AI_C1;
}
let x3 = x * x * x;
let mut f_sum = 1.0;
let mut f_term = 1.0;
for k in 1..MAX_SERIES_TERMS {
let k3 = 3 * k;
f_term *= x3 * (k3 as f64 - 2.0) / ((k3 as f64) * ((k3 - 1) as f64) * ((k3 - 2) as f64));
f_sum += f_term;
if f_term.abs() < EPSILON * f_sum.abs() {
break;
}
}
let mut g_sum = x;
let mut g_term = x;
for k in 1..MAX_SERIES_TERMS {
let k3 = 3 * k;
g_term *= x3 * (k3 as f64 - 1.0) / (((k3 + 1) as f64) * (k3 as f64) * ((k3 - 1) as f64));
g_sum += g_term;
if g_term.abs() < EPSILON * g_sum.abs() {
break;
}
}
AI_C1 * f_sum - AI_C2 * g_sum
}
fn airy_bi_series(x: f64) -> f64 {
if x.abs() < 1e-10 {
return BI_C1;
}
let x3 = x * x * x;
let mut f_sum = 1.0;
let mut f_term = 1.0;
for k in 1..MAX_SERIES_TERMS {
let k3 = 3 * k;
f_term *= x3 * (k3 as f64 - 2.0) / ((k3 as f64) * ((k3 - 1) as f64) * ((k3 - 2) as f64));
f_sum += f_term;
if f_term.abs() < EPSILON * f_sum.abs() {
break;
}
}
let mut g_sum = x;
let mut g_term = x;
for k in 1..MAX_SERIES_TERMS {
let k3 = 3 * k;
g_term *= x3 * (k3 as f64 - 1.0) / (((k3 + 1) as f64) * (k3 as f64) * ((k3 - 1) as f64));
g_sum += g_term;
if g_term.abs() < EPSILON * g_sum.abs() {
break;
}
}
BI_C1 * f_sum + BI_C2 * g_sum
}
fn airy_ai_asymp_pos(x: f64) -> f64 {
let zeta = (2.0 / 3.0) * x.powf(1.5);
let x14 = x.powf(0.25);
let prefactor = (-zeta).exp() / (2.0 * PI.sqrt() * x14);
let z = 1.0 / zeta;
let sum = asymp_u_series(z);
prefactor * sum
}
fn airy_bi_asymp_pos(x: f64) -> f64 {
let zeta = (2.0 / 3.0) * x.powf(1.5);
let x14 = x.powf(0.25);
let prefactor = zeta.exp() / (PI.sqrt() * x14);
let z = 1.0 / zeta;
let sum = asymp_u_series(z);
prefactor * sum
}
fn airy_ai_asymp_neg(x: f64) -> f64 {
let ax = x.abs();
let zeta = (2.0 / 3.0) * ax.powf(1.5);
let x14 = ax.powf(0.25);
let phase = zeta + PI / 4.0;
INV_SQRT_PI / x14 * phase.sin()
}
fn airy_bi_asymp_neg(x: f64) -> f64 {
let ax = x.abs();
let zeta = (2.0 / 3.0) * ax.powf(1.5);
let x14 = ax.powf(0.25);
let phase = zeta + PI / 4.0;
INV_SQRT_PI / x14 * phase.cos()
}
fn asymp_u_series(z: f64) -> f64 {
const U: [f64; 6] = [
1.0,
5.0 / 72.0,
5.0 * 7.0 * 11.0 / (2.0 * 72.0 * 72.0),
5.0 * 7.0 * 11.0 * 13.0 * 17.0 / (6.0 * 72.0 * 72.0 * 72.0),
5.0 * 7.0 * 11.0 * 13.0 * 17.0 * 19.0 * 23.0 / (24.0 * 26873856.0),
5.0 * 7.0 * 11.0 * 13.0 * 17.0 * 19.0 * 23.0 * 25.0 * 29.0 / (120.0 * 1934917632.0),
];
let mut sum = U[0];
let mut zk = z;
for &u in &U[1..] {
sum += u * zk;
zk *= z;
}
sum
}
#[cfg(test)]
mod tests {
use super::*;
const TOL: f64 = 1e-8;
fn assert_close(a: f64, b: f64, tol: f64, msg: &str) {
let diff = (a - b).abs();
assert!(
diff < tol || (a.is_nan() && b.is_nan()),
"{}: expected {}, got {}, diff {}",
msg,
b,
a,
diff
);
}
#[test]
fn test_airy_ai_special_values() {
assert_close(airy_ai_scalar(0.0), 0.3550280538878172, TOL, "Ai(0)");
assert_close(airy_ai_scalar(1.0), 0.1352924163128814, TOL, "Ai(1)");
assert_close(airy_ai_scalar(-1.0), 0.5355608832923521, TOL, "Ai(-1)");
assert!(airy_ai_scalar(10.0) < 1e-10);
assert!(airy_ai_scalar(20.0) < 1e-20);
}
#[test]
fn test_airy_bi_special_values() {
assert_close(airy_bi_scalar(0.0), 0.6149266274460007, TOL, "Bi(0)");
assert_close(airy_bi_scalar(1.0), 1.2074235949528714, TOL, "Bi(1)");
assert_close(airy_bi_scalar(-1.0), 0.10399738949694461, TOL, "Bi(-1)");
assert!(airy_bi_scalar(10.0) > 1e8);
}
#[test]
fn test_airy_wronskian() {
let ai0 = AI_C1;
let bi0 = BI_C1;
let ai_prime_0 = -AI_C2;
let bi_prime_0 = BI_C2;
let wronskian = ai0 * bi_prime_0 - ai_prime_0 * bi0;
assert_close(wronskian, 1.0 / PI, 1e-10, "Wronskian at x=0");
}
#[test]
fn test_airy_nan() {
assert!(airy_ai_scalar(f64::NAN).is_nan());
assert!(airy_bi_scalar(f64::NAN).is_nan());
}
#[test]
fn test_airy_oscillation_negative() {
let mut ai_signs = Vec::new();
let mut bi_signs = Vec::new();
for i in 0..20 {
let x = -(i as f64 + 0.5);
ai_signs.push(airy_ai_scalar(x) > 0.0);
bi_signs.push(airy_bi_scalar(x) > 0.0);
}
let ai_changes: usize = ai_signs.windows(2).filter(|w| w[0] != w[1]).count();
let bi_changes: usize = bi_signs.windows(2).filter(|w| w[0] != w[1]).count();
assert!(ai_changes > 0, "Ai should oscillate for x < 0");
assert!(bi_changes > 0, "Bi should oscillate for x < 0");
}
}