use crate::Result;
use crate::error::{ensure_finite, ensure_finite_output};
const SERIES_THRESHOLD: f64 = 0.5;
const MAX_SERIES_TERMS: usize = 32;
fn series_c(x: f64) -> f64 {
let mut sum = 0.5;
let mut term = sum;
for index in 0..MAX_SERIES_TERMS {
let n = 2.0 * index as f64;
term *= -x / ((n + 3.0) * (n + 4.0));
sum += term;
if term.abs() <= f64::EPSILON * sum.abs() {
break;
}
}
sum
}
fn series_s(x: f64) -> f64 {
let mut sum = 1.0 / 6.0;
let mut term = sum;
for index in 0..MAX_SERIES_TERMS {
let n = 2.0 * index as f64;
term *= -x / ((n + 4.0) * (n + 5.0));
sum += term;
if term.abs() <= f64::EPSILON * sum.abs() {
break;
}
}
sum
}
pub fn stumpff_c(x: f64) -> Result<f64> {
ensure_finite("x", x)?;
let value = if x.abs() < SERIES_THRESHOLD {
series_c(x)
} else if x > 0.0 {
let root = x.sqrt();
2.0 * (0.5 * root).sin().powi(2) / x
} else {
let minus_x = -x;
let root = minus_x.sqrt();
2.0 * (0.5 * root).sinh().powi(2) / minus_x
};
ensure_finite_output("stumpff_c", value)
}
pub fn stumpff_s(x: f64) -> Result<f64> {
ensure_finite("x", x)?;
let value = if x.abs() < SERIES_THRESHOLD {
series_s(x)
} else if x > 0.0 {
let root = x.sqrt();
(root - root.sin()) / root.powi(3)
} else {
let root = (-x).sqrt();
(root.sinh() - root) / root.powi(3)
};
ensure_finite_output("stumpff_s", value)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::PykepError;
fn close(actual: f64, expected: f64, tolerance: f64) {
assert!(
(actual - expected).abs() <= tolerance,
"{actual:.17e} != {expected:.17e}"
);
}
#[test]
fn exact_limits_are_returned_at_zero() {
assert_eq!(stumpff_c(0.0), Ok(0.5));
assert_eq!(stumpff_c(-0.0), Ok(0.5));
assert_eq!(stumpff_s(0.0), Ok(1.0 / 6.0));
}
#[test]
fn near_zero_series_avoids_catastrophic_cancellation() {
for x in [-1e-14, -1e-12, 1e-12, 1e-14] {
close(stumpff_c(x).unwrap(), 0.5 - x / 24.0, 2e-16);
close(stumpff_s(x).unwrap(), 1.0 / 6.0 - x / 120.0, 2e-16);
}
}
#[test]
fn ordinary_cpp_reference_values_match() {
close(stumpff_c(1.0).unwrap(), 0.459_697_694_131_860_3, 2e-16);
close(stumpff_s(1.0).unwrap(), 0.158_529_015_192_103_5, 2e-16);
close(stumpff_c(-1.0).unwrap(), 0.543_080_634_815_243_7, 2e-16);
close(stumpff_s(-1.0).unwrap(), 0.175_201_193_643_801_46, 2e-16);
}
#[test]
fn non_finite_and_overflowing_inputs_are_errors() {
assert_eq!(
stumpff_c(f64::NAN),
Err(PykepError::NonFiniteInput { parameter: "x" })
);
assert!(matches!(
stumpff_s(-1e7),
Err(PykepError::NumericalOverflow { .. })
));
}
}