use std::f64::consts::FRAC_PI_2;
use crate::errors::QlResult;
use crate::fail;
use crate::math::integrals::{Integrator, de_quadrature, require_accuracy};
use crate::types::{Real, Size};
const T_MAX: Real = 7.0;
pub struct ExpSinhIntegral {
rel_tolerance: Real,
max_refinements: Size,
}
impl ExpSinhIntegral {
pub fn new() -> Self {
ExpSinhIntegral {
rel_tolerance: Real::EPSILON.sqrt(),
max_refinements: 9,
}
}
pub fn with_params(rel_tolerance: Real, max_refinements: Size) -> QlResult<Self> {
require_accuracy(rel_tolerance)?;
Ok(ExpSinhIntegral {
rel_tolerance,
max_refinements,
})
}
pub fn integrate_semi_infinite<F>(&self, mut f: F) -> QlResult<Real>
where
F: FnMut(Real) -> Real,
{
self.semi_infinite(&mut f)
}
fn semi_infinite<F>(&self, f: &mut F) -> QlResult<Real>
where
F: FnMut(Real) -> Real,
{
let node = |t: Real| {
let x = (FRAC_PI_2 * t.sinh()).exp();
if x < Real::MIN_POSITIVE {
return None;
}
let w = x * FRAC_PI_2 * t.cosh();
if !w.is_finite() {
return None;
}
Some((x, w))
};
de_quadrature(f, node, T_MAX, self.rel_tolerance, self.max_refinements)
}
}
impl Default for ExpSinhIntegral {
fn default() -> Self {
ExpSinhIntegral::new()
}
}
impl Integrator for ExpSinhIntegral {
fn integrate_impl<F>(&self, f: &mut F, a: Real, b: Real) -> QlResult<Real>
where
F: FnMut(Real) -> Real,
{
if a <= -Real::MAX && b >= Real::MAX {
fail!(
"doubly infinite domains require a sinh-sinh quadrature, which is not ported; got [{a}, {b}]"
);
}
if b >= Real::MAX {
self.semi_infinite(&mut |u: Real| f(a + u))
} else if a <= -Real::MAX {
self.semi_infinite(&mut |u: Real| f(b - u))
} else {
fail!("exp-sinh quadrature integrates semi-infinite domains only, got [{a}, {b}]");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::math::distributions::normal::NormalDistribution;
const TOL: Real = 1e-6;
#[test]
fn matches_known_integrals() {
let integrator = ExpSinhIntegral::new();
let g = NormalDistribution::standard();
assert!(
(integrator
.integrate(|x| g.value(x), 0.0, Real::MAX)
.unwrap()
- 0.5)
.abs()
< TOL
);
assert!(
(integrator
.integrate(|x| x * (-x).exp(), 0.0, Real::MAX)
.unwrap()
- 1.0)
.abs()
< TOL
);
}
#[test]
fn native_semi_infinite_overload_matches() {
let integrator = ExpSinhIntegral::new();
let g = NormalDistribution::standard();
assert!((integrator.integrate_semi_infinite(|x| g.value(x)).unwrap() - 0.5).abs() < TOL);
assert!(
(integrator
.integrate_semi_infinite(|x| x * (-x).exp())
.unwrap()
- 1.0)
.abs()
< TOL
);
}
#[test]
fn shifted_and_reflected_domains() {
let integrator = ExpSinhIntegral::new();
assert!(
(integrator
.integrate(|x| (-(x - 1.0)).exp(), 1.0, Real::MAX)
.unwrap()
- 1.0)
.abs()
< TOL
);
assert!((integrator.integrate(|x| x.exp(), -Real::MAX, 0.0).unwrap() - 1.0).abs() < TOL);
}
#[test]
fn rejects_unsupported_domains() {
let integrator = ExpSinhIntegral::new();
assert!(integrator.integrate(|x| x, 0.0, 1.0).is_err());
assert!(
integrator
.integrate(|_| 0.0, -Real::MAX, Real::MAX)
.is_err()
);
assert!(
integrator
.integrate(|x| (-x).exp(), 0.0, Real::INFINITY)
.is_err()
);
}
#[test]
fn truncates_dead_tails_before_the_integrand_overflows() {
let integrator = ExpSinhIntegral::new();
let expected = std::f64::consts::PI.sqrt() / 4.0;
assert!(
(integrator
.integrate_semi_infinite(|x| x * x * (-x * x).exp())
.unwrap()
- expected)
.abs()
< TOL
);
}
#[test]
fn reports_non_convergence_on_divergent_integrand() {
let integrator = ExpSinhIntegral::new();
assert!(
integrator
.integrate_semi_infinite(|x| 1.0 / (1.0 + x))
.is_err()
);
}
#[test]
fn invalid_configuration_rejected() {
for tol in [0.0, -1.0, Real::EPSILON, Real::NAN, Real::INFINITY] {
assert!(
ExpSinhIntegral::with_params(tol, 9).is_err(),
"tolerance {tol}"
);
}
}
}