pub mod nk_functions;
pub mod types;
pub use nk_functions::{
nk0_asymptotic_check, nk0_boundary_check, nk_brinkman, nk_darcy_lapwood, nk_i, nk_i_prime,
NieldKuznetsov,
};
pub use types::{NKConfig, NKFunctionType, NKMethod, NKPhysicalModel, NKResult};
use std::f64::consts::PI;
use crate::error::{SpecialError, SpecialResult};
#[derive(Debug, Clone)]
pub struct NkConfig {
pub tol: f64,
pub max_terms: usize,
}
impl Default for NkConfig {
fn default() -> Self {
NkConfig {
tol: 1e-12,
max_terms: 200,
}
}
}
pub fn airy_ai(x: f64) -> f64 {
let ai0 = 0.355028053887817_f64; let aip0 = -0.258819403792807_f64;
if x.abs() < 5.0 {
airy_series(x, ai0, aip0)
} else if x > 0.0 {
let xi = 2.0 / 3.0 * x.powf(1.5);
let prefactor = 0.5 / PI.sqrt() * x.powf(-0.25);
prefactor * (-xi).exp() * airy_asymp_factor(xi, 1)
} else {
let ax = (-x).powf(0.75);
let xi = 2.0 / 3.0 * ax;
let prefactor = PI.powf(-0.5) * (-x).powf(-0.25);
prefactor * (xi + PI / 4.0).sin() * airy_osc_factor(xi, 1)
}
}
pub fn airy_bi(x: f64) -> f64 {
let bi0 = 0.614926627446001_f64; let bip0 = 0.448288356353826_f64;
if x.abs() < 5.0 {
airy_series(x, bi0, bip0)
} else if x > 0.0 {
let xi = 2.0 / 3.0 * x.powf(1.5);
let prefactor = PI.powf(-0.5) * x.powf(-0.25);
if xi > 700.0 {
return f64::INFINITY;
}
prefactor * xi.exp() * airy_asymp_factor(xi, -1)
} else {
let ax = (-x).powf(0.75);
let xi = 2.0 / 3.0 * ax;
let prefactor = PI.powf(-0.5) * (-x).powf(-0.25);
prefactor * (xi + PI / 4.0).cos() * airy_osc_factor(xi, -1)
}
}
pub fn airy_ai_prime(x: f64) -> f64 {
let ai0 = 0.355028053887817_f64;
let aip0 = -0.258819403792807_f64;
if x.abs() < 5.0 {
airy_series_prime(x, ai0, aip0)
} else if x > 0.0 {
let xi = 2.0 / 3.0 * x.powf(1.5);
let prefactor = -0.5 / PI.sqrt() * x.powf(0.25);
prefactor * (-xi).exp() * airy_asymp_factor(xi, 1)
} else {
let ax = (-x).powf(0.75);
let xi = 2.0 / 3.0 * ax;
let prefactor = -PI.powf(-0.5) * (-x).powf(0.25);
prefactor * (xi + PI / 4.0).cos() * airy_osc_factor(xi, 1)
}
}
pub fn airy_bi_prime(x: f64) -> f64 {
let bi0 = 0.614926627446001_f64;
let bip0 = 0.448288356353826_f64;
if x.abs() < 5.0 {
airy_series_prime(x, bi0, bip0)
} else if x > 0.0 {
let xi = 2.0 / 3.0 * x.powf(1.5);
if xi > 700.0 {
return f64::INFINITY;
}
let prefactor = PI.powf(-0.5) * x.powf(0.25);
prefactor * xi.exp() * airy_asymp_factor(xi, -1)
} else {
let ax = (-x).powf(0.75);
let xi = 2.0 / 3.0 * ax;
let prefactor = PI.powf(-0.5) * (-x).powf(0.25);
-prefactor * (xi + PI / 4.0).sin() * airy_osc_factor(xi, -1)
}
}
fn airy_series(x: f64, c0: f64, c1: f64) -> f64 {
let n_terms = 60usize;
let mut f_coeffs = vec![0.0_f64; n_terms + 3];
let mut g_coeffs = vec![0.0_f64; n_terms + 3];
f_coeffs[0] = 1.0;
f_coeffs[1] = 0.0;
g_coeffs[0] = 0.0;
g_coeffs[1] = 1.0;
for n in 0..n_terms {
if n >= 1 {
f_coeffs[n + 2] = f_coeffs[n - 1] / ((n + 1) * (n + 2)) as f64;
g_coeffs[n + 2] = g_coeffs[n - 1] / ((n + 1) * (n + 2)) as f64;
} else {
f_coeffs[2] = 0.0;
g_coeffs[2] = 0.0;
}
}
let mut f_val = 0.0_f64;
let mut g_val = 0.0_f64;
for i in (0..=n_terms).rev() {
f_val = f_val * x + f_coeffs[n_terms - i];
g_val = g_val * x + g_coeffs[n_terms - i];
}
f_val = 0.0;
g_val = 0.0;
for i in (0..=n_terms).rev() {
f_val = f_val * x + f_coeffs[i];
g_val = g_val * x + g_coeffs[i];
}
c0 * f_val + c1 * g_val
}
fn airy_series_prime(x: f64, c0: f64, c1: f64) -> f64 {
let n_terms = 60usize;
let mut f_coeffs = vec![0.0_f64; n_terms + 3];
let mut g_coeffs = vec![0.0_f64; n_terms + 3];
f_coeffs[0] = 1.0;
f_coeffs[1] = 0.0;
g_coeffs[0] = 0.0;
g_coeffs[1] = 1.0;
for n in 0..n_terms {
if n >= 1 {
f_coeffs[n + 2] = f_coeffs[n - 1] / ((n + 1) * (n + 2)) as f64;
g_coeffs[n + 2] = g_coeffs[n - 1] / ((n + 1) * (n + 2)) as f64;
} else {
f_coeffs[2] = 0.0;
g_coeffs[2] = 0.0;
}
}
let mut fp_val = 0.0_f64;
let mut gp_val = 0.0_f64;
for n in 1..=n_terms {
fp_val += n as f64 * f_coeffs[n] * x.powi((n - 1) as i32);
gp_val += n as f64 * g_coeffs[n] * x.powi((n - 1) as i32);
}
c0 * fp_val + c1 * gp_val
}
fn airy_asymp_factor(xi: f64, sign: i32) -> f64 {
let mut result = 1.0_f64;
let mut u = 1.0_f64;
let mut xi_pow = xi;
for s in 1..=8usize {
let num = ((6 * s - 1) * (6 * s - 3) * (6 * s - 5)) as f64;
let den = 216.0 * s as f64;
u *= num / den;
let term = (sign as f64).powi(s as i32) * u / xi_pow;
result += term;
if term.abs() < 1e-15 * result.abs() {
break;
}
xi_pow *= xi;
}
result
}
fn airy_osc_factor(xi: f64, _sign: i32) -> f64 {
let _ = xi;
1.0
}
pub fn nk_wronskian_check(x: f64) -> f64 {
let ai = airy_ai(x);
let bi = airy_bi(x);
let aip = airy_ai_prime(x);
let bip = airy_bi_prime(x);
ai * bip - aip * bi
}
fn gauss_legendre_nodes_weights() -> (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)
}
pub(crate) fn integrate_composite_gl<F>(f: F, a: f64, b: f64, n_sub: usize) -> f64
where
F: Fn(f64) -> f64,
{
let (nodes, weights) = gauss_legendre_nodes_weights();
let step = (b - a) / n_sub as f64;
let mut total = 0.0_f64;
for i in 0..n_sub {
let left = a + i as f64 * step;
let right = left + step;
let mid = (left + right) / 2.0;
let half = step / 2.0;
let sub: f64 = nodes
.iter()
.zip(weights.iter())
.map(|(&xi, &wi)| {
let t = mid + half * xi;
wi * f(t)
})
.sum();
total += sub * half;
}
total
}
pub(crate) fn particular_ai(x: f64, n_sub: usize) -> f64 {
let int_biait = if x >= 0.0 {
integrate_composite_gl(|t| airy_bi(t) * airy_ai(t), 0.0, x, n_sub)
} else {
-integrate_composite_gl(|t| airy_bi(t) * airy_ai(t), x, 0.0, n_sub)
};
let int_ai2 = if x >= 0.0 {
integrate_composite_gl(|t| airy_ai(t) * airy_ai(t), 0.0, x, n_sub)
} else {
-integrate_composite_gl(|t| airy_ai(t) * airy_ai(t), x, 0.0, n_sub)
};
PI * (-airy_ai(x) * int_biait + airy_bi(x) * int_ai2)
}
pub(crate) fn particular_bi(x: f64, n_sub: usize) -> f64 {
let int_bi2 = if x >= 0.0 {
integrate_composite_gl(|t| airy_bi(t) * airy_bi(t), 0.0, x, n_sub)
} else {
-integrate_composite_gl(|t| airy_bi(t) * airy_bi(t), x, 0.0, n_sub)
};
let int_aibit = if x >= 0.0 {
integrate_composite_gl(|t| airy_ai(t) * airy_bi(t), 0.0, x, n_sub)
} else {
-integrate_composite_gl(|t| airy_ai(t) * airy_bi(t), x, 0.0, n_sub)
};
PI * (-airy_ai(x) * int_bi2 + airy_bi(x) * int_aibit)
}
pub fn nield_kuznetsov_ni(a: f64, b: f64, x: f64, config: &NkConfig) -> SpecialResult<f64> {
let n_sub = config.max_terms / 4; let n_sub = n_sub.max(8);
let x_safe = if x > 5.0 { 5.0 } else { x };
let yp_ai_val = if a.abs() > 0.0 {
a * particular_ai(x_safe, n_sub)
} else {
0.0
};
let yp_bi_val = if b.abs() > 0.0 {
b * particular_bi(x_safe, n_sub)
} else {
0.0
};
Ok(yp_ai_val + yp_bi_val)
}
pub fn nield_kuznetsov_ki(a: f64, b: f64, x: f64, config: &NkConfig) -> SpecialResult<f64> {
let n_sub = (config.max_terms / 4).max(8);
let t_large = 8.0_f64;
let int_ai2_inf = integrate_composite_gl(|t| airy_ai(t) * airy_ai(t), 0.0, t_large, n_sub);
let int_aibi_inf = integrate_composite_gl(|t| airy_ai(t) * airy_bi(t), 0.0, t_large, n_sub);
let c_correction = PI * (a * int_ai2_inf + b * int_aibi_inf);
let ni_val = nield_kuznetsov_ni(a, b, x, config)?;
Ok(ni_val + c_correction * airy_ai(x))
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::PI;
#[test]
fn test_wronskian_near_zero() {
for &x in &[-1.0, 0.0, 1.0, 2.0] {
let w = nk_wronskian_check(x);
let expected = 1.0 / PI;
assert!(
(w - expected).abs() < 1e-6,
"Wronskian at x={x}: expected {expected}, got {w}"
);
}
}
#[test]
fn test_airy_ai_at_zero() {
let val = airy_ai(0.0);
assert!((val - 0.355028053887817).abs() < 1e-10, "Ai(0) = {val}");
}
#[test]
fn test_airy_bi_at_zero() {
let val = airy_bi(0.0);
assert!((val - 0.614926627446001).abs() < 1e-10, "Bi(0) = {val}");
}
#[test]
fn test_wronskian_explicit() {
let ai0 = airy_ai(0.0);
let bi0 = airy_bi(0.0);
let aip0 = airy_ai_prime(0.0);
let bip0 = airy_bi_prime(0.0);
let w = ai0 * bip0 - aip0 * bi0;
let expected = 1.0 / PI;
assert!(
(w - expected).abs() < 1e-8,
"W(Ai,Bi)(0) = {w}, expected {expected}"
);
}
#[test]
fn test_ni_at_zero() {
let config = NkConfig::default();
let val = nield_kuznetsov_ni(1.0, 0.0, 0.0, &config).unwrap();
assert!(val.abs() < 1e-14, "Ni(1,0,0) should be 0, got {val}");
}
#[test]
fn test_ni_finite() {
let config = NkConfig {
max_terms: 40,
tol: 1e-10,
};
let val = nield_kuznetsov_ni(1.0, 0.0, 1.0, &config).unwrap();
assert!(val.is_finite(), "Ni(1,0,1) should be finite, got {val}");
}
#[test]
fn test_ki_finite() {
let config = NkConfig {
max_terms: 40,
tol: 1e-10,
};
let val = nield_kuznetsov_ki(1.0, 0.0, 1.0, &config).unwrap();
assert!(val.is_finite(), "Ki(1,0,1) should be finite, got {val}");
}
#[test]
fn test_ni_derivative_equation() {
let config = NkConfig {
max_terms: 80,
tol: 1e-11,
};
let a = 1.0_f64;
let b = 0.0_f64;
let x = 0.5_f64;
let h = 1e-5_f64;
let ni_x = nield_kuznetsov_ni(a, b, x, &config).unwrap();
let ni_xph = nield_kuznetsov_ni(a, b, x + h, &config).unwrap();
let ni_xmh = nield_kuznetsov_ni(a, b, x - h, &config).unwrap();
let ni_pp = (ni_xph - 2.0 * ni_x + ni_xmh) / (h * h);
let rhs = a * airy_ai(x) + b * airy_bi(x);
let lhs = ni_pp - x * ni_x;
assert!(
(lhs - rhs).abs() < 1e-5,
"Ni'' - x Ni = a Ai + b Bi check: lhs={lhs}, rhs={rhs}"
);
}
#[test]
fn test_airy_series_consistency() {
let ai_series = airy_series(1.0, 0.355028053887817, -0.258819403792807);
assert!(
(ai_series - 0.1352924163).abs() < 1e-7,
"Ai(1) series = {ai_series}"
);
}
}