use crate::adaptive::AdaptiveIntegrator;
use crate::error::QuadratureError;
use crate::result::QuadratureResult;
#[cfg(not(feature = "std"))]
use num_traits::Float as _;
#[derive(Debug, Clone)]
pub struct CauchyPV {
abs_tol: f64,
rel_tol: f64,
max_evals: usize,
}
impl Default for CauchyPV {
fn default() -> Self {
Self {
abs_tol: 1.49e-8,
rel_tol: 1.49e-8,
max_evals: 500_000,
}
}
}
impl CauchyPV {
#[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
}
#[must_use]
pub fn with_max_evals(mut self, n: usize) -> Self {
self.max_evals = n;
self
}
#[allow(clippy::many_single_char_names)] pub fn integrate<G>(
&self,
a: f64,
b: f64,
c: f64,
f: G,
) -> Result<QuadratureResult<f64>, QuadratureError>
where
G: Fn(f64) -> f64,
{
if !a.is_finite() || !b.is_finite() || !c.is_finite() {
return Err(QuadratureError::DegenerateInterval);
}
let (lo, hi) = if a < b { (a, b) } else { (b, a) };
if c <= lo || c >= hi {
return Err(QuadratureError::InvalidInput(
"singularity c must be strictly inside (a, b)",
));
}
let fc = f(c);
if !fc.is_finite() {
return Err(QuadratureError::InvalidInput(
"f(c) must be finite for the subtraction technique",
));
}
let log_term = fc * ((b - c) / (c - a)).ln();
let guard = 1e-15 * (b - a).abs();
let g = |x: f64| -> f64 {
if (x - c).abs() < guard {
0.0
} else {
(f(x) - fc) / (x - c)
}
};
let adaptive_result = AdaptiveIntegrator::default()
.with_abs_tol(self.abs_tol)
.with_rel_tol(self.rel_tol)
.with_max_evals(self.max_evals)
.integrate_with_breaks(a, b, &[c], g)?;
Ok(QuadratureResult {
value: adaptive_result.value + log_term,
error_estimate: adaptive_result.error_estimate,
num_evals: adaptive_result.num_evals + 1, converged: adaptive_result.converged,
})
}
}
pub fn pv_integrate<G>(
f: G,
a: f64,
b: f64,
c: f64,
tol: f64,
) -> Result<QuadratureResult<f64>, QuadratureError>
where
G: Fn(f64) -> f64,
{
CauchyPV::default()
.with_abs_tol(tol)
.with_rel_tol(tol)
.integrate(a, b, c, f)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn symmetric_constant() {
let result = pv_integrate(|_| 1.0, 0.0, 1.0, 0.5, 1e-10).unwrap();
assert!(
result.value.abs() < 1e-8,
"value={}, expected 0",
result.value
);
}
#[test]
fn odd_integrand() {
let result = pv_integrate(|_| 1.0, -1.0, 1.0, 0.0, 1e-10).unwrap();
assert!(
result.value.abs() < 1e-8,
"value={}, expected 0",
result.value
);
}
#[test]
fn polynomial_f() {
let exact = 0.8 + 0.09 * (7.0_f64 / 3.0).ln();
let result = pv_integrate(|x| x * x, 0.0, 1.0, 0.3, 1e-10).unwrap();
assert!(
(result.value - exact).abs() < 1e-7,
"value={}, expected={exact}",
result.value
);
}
#[test]
fn linear_f() {
let result = pv_integrate(|x| x, 0.0, 2.0, 1.0, 1e-10).unwrap();
assert!(
(result.value - 2.0).abs() < 1e-7,
"value={}, expected=2",
result.value
);
}
#[test]
fn c_outside_interval() {
assert!(pv_integrate(|x| x, 0.0, 1.0, 2.0, 1e-10).is_err());
}
#[test]
fn c_at_endpoint() {
assert!(pv_integrate(|x| x, 0.0, 1.0, 0.0, 1e-10).is_err());
assert!(pv_integrate(|x| x, 0.0, 1.0, 1.0, 1e-10).is_err());
}
#[test]
fn reversed_bounds() {
let exact = -(0.8 + 0.09 * (7.0_f64 / 3.0).ln());
let result = pv_integrate(|x| x * x, 1.0, 0.0, 0.3, 1e-10).unwrap();
assert!(
(result.value - exact).abs() < 1e-7,
"value={}, expected={exact}",
result.value
);
}
#[test]
fn nan_inputs() {
assert!(pv_integrate(|x| x, f64::NAN, 1.0, 0.5, 1e-10).is_err());
assert!(pv_integrate(|x| x, 0.0, 1.0, f64::NAN, 1e-10).is_err());
}
#[test]
fn inf_inputs_rejected() {
assert!(pv_integrate(|x| x, f64::INFINITY, 1.0, 0.5, 1e-10).is_err());
assert!(pv_integrate(|x| x, 0.0, f64::NEG_INFINITY, 0.5, 1e-10).is_err());
assert!(pv_integrate(|x| x, 0.0, 1.0, f64::INFINITY, 1e-10).is_err());
}
}