use crate::error::QuadratureError;
use crate::result::QuadratureResult;
#[cfg(not(feature = "std"))]
use num_traits::Float as _;
#[derive(Debug, Clone)]
pub struct TanhSinh {
max_levels: usize,
abs_tol: f64,
rel_tol: f64,
}
impl Default for TanhSinh {
fn default() -> Self {
Self {
max_levels: 12,
abs_tol: 1.49e-8,
rel_tol: 1.49e-8,
}
}
}
impl TanhSinh {
#[must_use]
pub fn with_max_levels(mut self, levels: usize) -> Self {
self.max_levels = levels;
self
}
#[must_use]
pub fn with_abs_tol(mut self, tol: f64) -> Self {
self.abs_tol = tol;
self
}
#[must_use]
pub fn with_rel_tol(mut self, tol: f64) -> Self {
self.rel_tol = tol;
self
}
#[allow(clippy::many_single_char_names)] #[allow(clippy::cast_precision_loss)] #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] #[allow(clippy::too_many_lines)] pub fn integrate<G>(
&self,
a: f64,
b: f64,
f: G,
) -> Result<QuadratureResult<f64>, QuadratureError>
where
G: Fn(f64) -> f64,
{
if !a.is_finite() || !b.is_finite() {
return Err(QuadratureError::DegenerateInterval);
}
if (b - a).abs() < f64::EPSILON {
return Ok(QuadratureResult {
value: 0.0,
error_estimate: 0.0,
num_evals: 0,
converged: true,
});
}
let mid = 0.5 * (a + b);
let half = 0.5 * (b - a);
let pi_2 = core::f64::consts::FRAC_PI_2;
let mut total_evals = 0usize;
let mut prev_prev_estimate: f64 = 0.0;
let mut prev_estimate: f64 = 0.0;
let mut h: f64 = 1.0;
for level in 0..=self.max_levels {
let step = if level == 0 { 1 } else { 2 };
let start = 1;
if level > 0 {
h *= 0.5;
}
let mut new_sum = 0.0;
if level == 0 {
let w0 = pi_2;
let fval = f(mid);
if fval.is_finite() {
new_sum += w0 * fval;
}
total_evals += 1;
}
let max_k = (7.0 / h).ceil() as usize;
let mut k = start;
let mut consecutive_tiny = 0;
while k <= max_k {
let t = k as f64 * h;
let sinh_t = t.sinh();
let cosh_t = t.cosh();
let u = pi_2 * sinh_t;
if u.abs() > 710.0 {
break;
}
let cosh_u = u.cosh();
let tanh_u = u.tanh();
let weight = pi_2 * cosh_t / (cosh_u * cosh_u);
if weight < 1e-300 {
break;
}
let mut local_contrib = 0.0_f64;
let x_pos = mid + half * tanh_u;
if x_pos > a && x_pos < b {
let fval = f(x_pos);
if fval.is_finite() {
new_sum += weight * fval;
local_contrib = local_contrib.max((weight * fval).abs());
}
total_evals += 1;
}
let x_neg = mid - half * tanh_u;
if x_neg > a && x_neg < b {
let fval = f(x_neg);
if fval.is_finite() {
new_sum += weight * fval;
local_contrib = local_contrib.max((weight * fval).abs());
}
total_evals += 1;
}
let threshold = f64::EPSILON * prev_estimate.abs().max(1e-300);
if local_contrib * half < threshold {
consecutive_tiny += 1;
if consecutive_tiny >= 3 {
break;
}
} else {
consecutive_tiny = 0;
}
k += step;
}
let estimate = if level == 0 {
h * half * new_sum
} else {
0.5 * prev_estimate + h * half * new_sum
};
if level >= 3 {
let error = (estimate - prev_estimate).abs();
let tol = self.abs_tol.max(self.rel_tol * estimate.abs());
if error <= tol {
return Ok(QuadratureResult {
value: estimate,
error_estimate: error,
num_evals: total_evals,
converged: true,
});
}
}
prev_prev_estimate = prev_estimate;
prev_estimate = estimate;
}
let error = if self.max_levels > 0 {
(prev_estimate - prev_prev_estimate).abs()
} else {
f64::INFINITY
};
Ok(QuadratureResult {
value: prev_estimate,
error_estimate: error,
num_evals: total_evals,
converged: false,
})
}
}
pub fn tanh_sinh_integrate<G>(
f: G,
a: f64,
b: f64,
tol: f64,
) -> Result<QuadratureResult<f64>, QuadratureError>
where
G: Fn(f64) -> f64,
{
TanhSinh::default()
.with_abs_tol(tol)
.with_rel_tol(tol)
.integrate(a, b, f)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn smooth_polynomial() {
let result = tanh_sinh_integrate(|x| x * x, 0.0, 1.0, 1e-12).unwrap();
assert!(
(result.value - 1.0 / 3.0).abs() < 1e-10,
"value={}",
result.value
);
assert!(result.converged);
}
#[test]
fn sqrt_singularity() {
let result = tanh_sinh_integrate(|x| 1.0 / x.sqrt(), 0.0, 1.0, 1e-10).unwrap();
assert!((result.value - 2.0).abs() < 1e-7, "value={}", result.value);
}
#[test]
fn log_singularity() {
let result = tanh_sinh_integrate(|x| x.ln(), 0.0, 1.0, 1e-10).unwrap();
assert!(
(result.value - (-1.0)).abs() < 1e-8,
"value={}",
result.value
);
}
#[test]
fn strong_algebraic_singularity() {
let result = tanh_sinh_integrate(|x| x.powf(-0.75), 0.0, 1.0, 1e-6).unwrap();
assert!((result.value - 4.0).abs() < 0.01, "value={}", result.value);
}
#[test]
fn both_endpoints_singular() {
let result = tanh_sinh_integrate(|x| 1.0 / (x * (1.0 - x)).sqrt(), 0.0, 1.0, 1e-8).unwrap();
assert!(
(result.value - core::f64::consts::PI).abs() < 1e-6,
"value={}",
result.value
);
}
#[test]
fn chebyshev_weight() {
let result = tanh_sinh_integrate(|x| 1.0 / (1.0 - x * x).sqrt(), -1.0, 1.0, 1e-8).unwrap();
assert!(
(result.value - core::f64::consts::PI).abs() < 1e-6,
"value={}",
result.value
);
}
#[test]
fn sin_integral() {
let result = tanh_sinh_integrate(|x| x.sin(), 0.0, core::f64::consts::PI, 1e-10).unwrap();
assert!((result.value - 2.0).abs() < 1e-8, "value={}", result.value);
}
#[test]
fn zero_width_interval() {
let result = tanh_sinh_integrate(|x| x, 1.0, 1.0, 1e-10).unwrap();
assert_eq!(result.value, 0.0);
assert!(result.converged);
}
#[test]
fn nan_bounds() {
assert!(tanh_sinh_integrate(|x| x, f64::NAN, 1.0, 1e-10).is_err());
}
#[test]
fn inf_bounds_rejected() {
assert!(tanh_sinh_integrate(|x| x, f64::INFINITY, 1.0, 1e-10).is_err());
assert!(tanh_sinh_integrate(|x| x, 0.0, f64::NEG_INFINITY, 1e-10).is_err());
}
#[test]
fn non_convergence() {
let result = TanhSinh::default()
.with_max_levels(0)
.integrate(0.0, 1.0, |x| 1.0 / x.sqrt())
.unwrap();
assert!(!result.converged);
}
#[test]
fn non_convergence_error_not_fabricated() {
let tol = 1e-15;
let result = TanhSinh::default()
.with_max_levels(2)
.with_abs_tol(tol)
.with_rel_tol(tol)
.integrate(0.0, 1.0, |x| 1.0 / x.sqrt())
.unwrap();
assert!(!result.converged);
assert!(result.error_estimate.is_finite());
assert!((result.error_estimate - tol * 10.0).abs() > 1e-20);
}
}