use std::f64::consts::PI;
use super::types::{NKConfig, NKMethod, NKResult};
use super::{
airy_ai, airy_ai_prime, airy_bi, airy_bi_prime, integrate_composite_gl, particular_ai, NkConfig,
};
use crate::error::{SpecialError, SpecialResult};
#[derive(Debug, Clone)]
pub struct NieldKuznetsov {
pub config: NKConfig,
}
impl NieldKuznetsov {
pub fn new(config: NKConfig) -> Self {
NieldKuznetsov { config }
}
pub fn with_defaults() -> Self {
NieldKuznetsov {
config: NKConfig::default(),
}
}
pub fn evaluate(&self, i: usize, x: f64) -> SpecialResult<NKResult> {
let (value, method) = if x.abs() > self.config.asymptotic_threshold && i == 0 {
(self.nk0_asymptotic(x)?, NKMethod::AsymptoticLarge)
} else if x.abs() < 0.5 && i <= 3 {
(self.nk_i_series(i, x)?, NKMethod::SeriesSmall)
} else if self.config.use_hypergeometric && i == 0 {
(self.nk0_hypergeometric(x)?, NKMethod::Hypergeometric)
} else {
(self.nk_i_quadrature(i, x)?, NKMethod::Quadrature)
};
Ok(NKResult {
value,
derivative: None,
index: i,
x,
error_estimate: None,
method,
})
}
pub fn evaluate_with_derivative(&self, i: usize, x: f64) -> SpecialResult<NKResult> {
let mut result = self.evaluate(i, x)?;
let deriv = if i > 0 {
self.nk_i_quadrature(i - 1, x)?
} else {
self.nk0_derivative(x)?
};
result.derivative = Some(deriv);
Ok(result)
}
fn nk0_quadrature(&self, x: f64) -> SpecialResult<f64> {
let n_sub = (self.config.max_terms / 8).max(4);
Ok(particular_ai(x, n_sub))
}
fn nk_i_quadrature(&self, i: usize, x: f64) -> SpecialResult<f64> {
if i == 0 {
return self.nk0_quadrature(x);
}
if i == 1 {
let n_sub = (self.config.max_terms / 8).max(4);
let result = if x >= 0.0 {
integrate_composite_gl(|t| self.nk0_quadrature(t).unwrap_or(0.0), 0.0, x, n_sub)
} else {
-integrate_composite_gl(|t| self.nk0_quadrature(t).unwrap_or(0.0), x, 0.0, n_sub)
};
return Ok(result);
}
let n_grid = 32usize;
let (a, b) = if x >= 0.0 { (0.0, x) } else { (x, 0.0) };
if (b - a).abs() < 1e-15 {
return Ok(0.0);
}
let h = (b - a) / n_grid as f64;
let mut vals = Vec::with_capacity(n_grid + 1);
for j in 0..=n_grid {
let t = a + j as f64 * h;
vals.push(self.nk0_quadrature(t).unwrap_or(0.0));
}
for _level in 0..i {
let mut integrated = Vec::with_capacity(vals.len());
integrated.push(0.0_f64);
let mut cumulative = 0.0_f64;
for j in 1..vals.len() {
cumulative += 0.5 * h * (vals[j - 1] + vals[j]);
integrated.push(cumulative);
}
vals = integrated;
}
if x >= 0.0 {
Ok(*vals.last().unwrap_or(&0.0))
} else {
Ok(-(*vals.last().unwrap_or(&0.0)))
}
}
fn nk0_asymptotic(&self, x: f64) -> SpecialResult<f64> {
if x > 0.0 {
let ai_x = airy_ai(x);
let mut sum = 1.0_f64;
let x3 = x * x * x;
let mut term_coeff = 1.0_f64;
let coeffs = [1.0, -5.0 / 2.0, 385.0 / 8.0, -85085.0 / 16.0];
for (k, &c) in coeffs.iter().enumerate().skip(1) {
if k >= self.config.asymptotic_terms {
break;
}
term_coeff /= x3;
let term = c * term_coeff;
if term.abs() < self.config.tol * sum.abs() {
break;
}
sum += term;
}
Ok(-ai_x * sum / (2.0 * x))
} else {
self.nk0_quadrature(x)
}
}
fn nk_i_series(&self, i: usize, x: f64) -> SpecialResult<f64> {
if i == 0 {
return self.nk0_series(x);
}
let nk0_coeffs = self.nk0_series_coefficients(20)?;
let mut coeffs = nk0_coeffs;
for _ in 0..i {
let mut new_coeffs = vec![0.0; coeffs.len() + 1];
for (n, &c) in coeffs.iter().enumerate() {
new_coeffs[n + 1] = c / (n + 1) as f64;
}
coeffs = new_coeffs;
}
let mut result = 0.0_f64;
let mut x_pow = 1.0_f64;
for &c in &coeffs {
result += c * x_pow;
x_pow *= x;
}
Ok(result)
}
fn nk0_series(&self, x: f64) -> SpecialResult<f64> {
let coeffs = self.nk0_series_coefficients(20)?;
let mut result = 0.0_f64;
let mut x_pow = 1.0_f64;
for &c in &coeffs {
result += c * x_pow;
x_pow *= x;
}
Ok(result)
}
fn nk0_series_coefficients(&self, n_terms: usize) -> SpecialResult<Vec<f64>> {
let ai0 = 0.355028053887817_f64; let aip0 = -0.258819403792807_f64;
let mut ai_coeffs = vec![0.0_f64; n_terms + 3];
ai_coeffs[0] = ai0;
ai_coeffs[1] = aip0;
ai_coeffs[2] = 0.0; for n in 1..(n_terms + 1) {
if n + 2 < ai_coeffs.len() && n >= 1 {
ai_coeffs[n + 2] = ai_coeffs[n - 1] / ((n + 1) * (n + 2)) as f64;
}
}
let mut y_coeffs = vec![0.0_f64; n_terms + 3];
y_coeffs[0] = 0.0;
y_coeffs[1] = 0.0;
y_coeffs[2] = ai_coeffs[0] / 2.0;
for n in 1..n_terms {
let rhs = if n >= 1 { y_coeffs[n - 1] } else { 0.0 } + ai_coeffs[n];
if n + 2 < y_coeffs.len() {
y_coeffs[n + 2] = rhs / ((n + 1) * (n + 2)) as f64;
}
}
Ok(y_coeffs[..n_terms].to_vec())
}
fn nk0_hypergeometric(&self, x: f64) -> SpecialResult<f64> {
if x.abs() < 0.5 {
return self.nk0_series(x);
}
if x > 0.0 {
let zeta = 2.0 / 3.0 * x.powf(1.5);
if zeta > 10.0 {
return self.nk0_asymptotic(x);
}
self.nk0_quadrature(x)
} else {
self.nk0_quadrature(x)
}
}
fn nk0_derivative(&self, x: f64) -> SpecialResult<f64> {
if x.abs() < 0.5 {
let coeffs = self.nk0_series_coefficients(20)?;
let mut result = 0.0_f64;
let mut x_pow = 1.0_f64;
for (n, &c) in coeffs.iter().enumerate().skip(1) {
result += n as f64 * c * x_pow;
x_pow *= x;
}
return Ok(result);
}
let n_sub = (self.config.max_terms / 4).max(8);
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)
};
let int_aibi = 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)
};
let bip_x = airy_bi_prime(x);
let aip_x = airy_ai_prime(x);
Ok(PI * (bip_x * int_ai2 - aip_x * int_aibi))
}
}
pub fn nk_i(i: usize, x: f64) -> SpecialResult<f64> {
let nk = NieldKuznetsov::with_defaults();
let result = nk.evaluate(i, x)?;
Ok(result.value)
}
pub fn nk_i_prime(i: usize, x: f64) -> SpecialResult<f64> {
let nk = NieldKuznetsov::with_defaults();
let result = nk.evaluate_with_derivative(i, x)?;
result
.derivative
.ok_or_else(|| SpecialError::ComputationError("Derivative not computed".to_string()))
}
pub fn nk_brinkman(x: f64) -> SpecialResult<f64> {
nk_i(0, x)
}
pub fn nk_darcy_lapwood(x: f64, darcy_number: f64) -> SpecialResult<f64> {
if darcy_number <= 0.0 {
return Err(SpecialError::DomainError(
"Darcy number must be positive".to_string(),
));
}
let da_third = darcy_number.powf(1.0 / 3.0);
let scaled_x = x / da_third;
let nk0 = nk_i(0, scaled_x)?;
Ok(da_third * nk0)
}
pub fn nk0_boundary_check() -> SpecialResult<f64> {
nk_i(0, 0.0)
}
pub fn nk0_asymptotic_check(x: f64) -> SpecialResult<(f64, f64)> {
let nk0_val = nk_i(0, x)?;
let asymptotic = -airy_ai(x) / (2.0 * x);
Ok((nk0_val, asymptotic))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_nk_struct_creation() {
let nk = NieldKuznetsov::with_defaults();
assert!((nk.config.tol - 1e-12).abs() < f64::EPSILON);
}
#[test]
fn test_nk0_at_zero() {
let val = nk_i(0, 0.0).expect("nk_i(0,0) failed");
assert!(val.abs() < 1e-12, "Nk_0(0) should be 0, got {val}");
}
#[test]
fn test_nk0_derivative_at_zero() {
let val = nk_i_prime(0, 0.0).expect("nk_i_prime(0,0) failed");
assert!(val.abs() < 1e-10, "Nk_0'(0) should be 0, got {val}");
}
#[test]
fn test_nk1_at_zero() {
let val = nk_i(1, 0.0).expect("nk_i(1,0) failed");
assert!(val.abs() < 1e-12, "Nk_1(0) should be 0, got {val}");
}
#[test]
fn test_nk0_finite() {
for &x in &[-2.0, -1.0, 0.5, 1.0, 2.0, 3.0] {
let val = nk_i(0, x).expect("nk_i(0, x) failed");
assert!(val.is_finite(), "Nk_0({x}) should be finite, got {val}");
}
}
#[test]
fn test_nk_higher_orders_finite() {
for &x in &[-1.0, 0.0, 0.5, 1.0, 2.0] {
let val = nk_i(1, x).expect("nk_i(1) failed");
assert!(val.is_finite(), "Nk_1({x}) should be finite, got {val}");
}
for i in 2..=3 {
let val = nk_i(i, 0.5).expect("nk_i failed");
assert!(val.is_finite(), "Nk_{i}(0.5) should be finite, got {val}");
}
}
#[test]
fn test_nk0_asymptotic_large_x() {
let x = 10.0;
let (actual, expected) = nk0_asymptotic_check(x).expect("asymptotic check failed");
assert!(
actual.abs() < 1e-3,
"Nk_0(10) should be small, got {actual}"
);
assert!(
expected.abs() < 1e-3,
"Asymptotic should be small, got {expected}"
);
if expected.abs() > 1e-15 {
let rel_err = ((actual - expected) / expected).abs();
assert!(rel_err < 10.0, "Asymptotic agreement: rel_err = {rel_err}");
}
}
#[test]
fn test_nk0_series_coefficients() {
let nk = NieldKuznetsov::with_defaults();
let coeffs = nk.nk0_series_coefficients(10).expect("coefficients failed");
assert_eq!(coeffs.len(), 10);
assert!(coeffs[0].abs() < 1e-15);
assert!(coeffs[1].abs() < 1e-15);
assert!((coeffs[2] - 0.355028053887817 / 2.0).abs() < 1e-10);
}
#[test]
fn test_nk_boundary_check() {
let val = nk0_boundary_check().expect("boundary check failed");
assert!(val.abs() < 1e-12, "Boundary check: Nk_0(0) = {val}");
}
#[test]
fn test_nk_brinkman() {
let val = nk_brinkman(1.0).expect("brinkman failed");
assert!(val.is_finite(), "Brinkman Nk_0(1) should be finite");
}
#[test]
fn test_nk_darcy_lapwood() {
let val = nk_darcy_lapwood(1.0, 0.01).expect("darcy-lapwood failed");
assert!(val.is_finite(), "Darcy-Lapwood value should be finite");
assert!(nk_darcy_lapwood(1.0, -0.01).is_err());
}
#[test]
fn test_nk_with_derivative() {
let nk = NieldKuznetsov::with_defaults();
let result = nk.evaluate_with_derivative(0, 1.0).expect("eval failed");
assert!(result.value.is_finite());
assert!(result.derivative.is_some());
assert!(result.derivative.expect("no deriv").is_finite());
}
#[test]
fn test_nk1_derivative_equals_nk0() {
let x = 0.5;
let nk0_val = nk_i(0, x).expect("nk0 failed");
let nk1_deriv = nk_i_prime(1, x).expect("nk1' failed");
assert!(
(nk0_val - nk1_deriv).abs() < 0.1,
"Nk_1'(x) = Nk_0(x) failed: {} vs {}",
nk1_deriv,
nk0_val
);
}
#[test]
fn test_nk_series_small_x() {
let nk = NieldKuznetsov::with_defaults();
let result = nk.evaluate(0, 0.1).expect("eval failed");
assert!(result.value.is_finite());
assert_eq!(result.method, NKMethod::SeriesSmall);
}
#[test]
fn test_nk0_ode_check() {
let nk = NieldKuznetsov::with_defaults();
let x = 0.5;
let h = 1e-4;
let y_m = nk.evaluate(0, x - h).expect("eval failed").value;
let y_0 = nk.evaluate(0, x).expect("eval failed").value;
let y_p = nk.evaluate(0, x + h).expect("eval failed").value;
let y_pp = (y_p - 2.0 * y_0 + y_m) / (h * h);
let lhs = y_pp - x * y_0;
let rhs = airy_ai(x);
assert!(
(lhs - rhs).abs() < 0.1,
"ODE check: lhs={lhs}, rhs={rhs}, diff={}",
(lhs - rhs).abs()
);
}
}