use crate::errors::QlResult;
use crate::fail;
use crate::math::comparison::close_enough;
use crate::math::integrals::Integrator;
use crate::types::Real;
pub struct PiecewiseIntegral<I> {
integrator: I,
critical_points: Vec<Real>,
avoid_critical_points: bool,
}
impl<I: Integrator> PiecewiseIntegral<I> {
pub fn new(
integrator: I,
critical_points: Vec<Real>,
avoid_critical_points: bool,
) -> QlResult<Self> {
if let Some(bad) = critical_points.iter().find(|p| !p.is_finite()) {
fail!("critical points must be finite, got {bad}");
}
let mut critical_points = critical_points;
critical_points.sort_by(Real::total_cmp);
critical_points.dedup_by(|a, b| close_enough(*a, *b));
Ok(PiecewiseIntegral {
integrator,
critical_points,
avoid_critical_points,
})
}
fn below(&self, p: Real) -> Real {
if self.avoid_critical_points {
p.next_down()
} else {
p
}
}
fn above(&self, p: Real) -> Real {
if self.avoid_critical_points {
p.next_up()
} else {
p
}
}
fn integrate_piece<F>(&self, f: &mut F, a: Real, b: Real) -> QlResult<Real>
where
F: FnMut(Real) -> Real,
{
if close_enough(a, b) {
Ok(0.0)
} else {
self.integrator.integrate(&mut *f, a, b)
}
}
}
impl<I: Integrator> Integrator for PiecewiseIntegral<I> {
fn integrate_impl<F>(&self, f: &mut F, a: Real, b: Real) -> QlResult<Real>
where
F: FnMut(Real) -> Real,
{
let points = &self.critical_points;
let a0 = points.partition_point(|&p| p < a);
let mut b0 = points.partition_point(|&p| p < b);
if a0 == points.len() {
let lower = match points.last() {
Some(&last) if close_enough(a, last) => self.above(a),
_ => a,
};
return self.integrate_piece(f, lower, b);
}
let mut res = 0.0;
if !close_enough(a, points[a0]) {
res += self.integrate_piece(f, a, self.below(points[a0]).min(b))?;
}
if b0 == points.len() {
b0 -= 1;
if !close_enough(points[b0], b) {
res += self.integrate_piece(f, self.above(points[b0]), b)?;
}
}
for i in a0..b0 {
res +=
self.integrate_piece(f, self.above(points[i]), self.below(points[i + 1]).min(b))?;
}
Ok(res)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::math::comparison::close;
use crate::math::integrals::segment::SegmentIntegral;
fn step(t: Real) -> Real {
let x = [1.0, 2.0, 3.0, 4.0, 5.0];
let y = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
let idx = x.partition_point(|&xi| xi <= t);
y[idx.min(y.len() - 1)]
}
#[test]
fn matches_step_function_areas() {
let piecewise = PiecewiseIntegral::new(
SegmentIntegral::new(1).unwrap(),
vec![1.0, 2.0, 3.0, 4.0, 5.0],
true,
)
.unwrap();
let cases = [
(-1.0, 0.0, 1.0),
(0.0, 1.0, 1.0),
(0.0, 1.5, 2.0),
(0.0, 2.0, 3.0),
(0.0, 2.5, 4.5),
(0.0, 3.0, 6.0),
(0.0, 4.0, 10.0),
(0.0, 5.0, 15.0),
(0.0, 6.0, 21.0),
(0.0, 7.0, 27.0),
(3.5, 4.5, 4.5),
(5.0, 10.0, 30.0),
(9.0, 10.0, 6.0),
];
for (a, b, expected) in cases {
let calculated = piecewise.integrate(step, a, b).unwrap();
assert!(
close(calculated, expected),
"[{a}, {b}]: calculated {calculated}, expected {expected}"
);
}
}
#[test]
fn nudges_off_zero_and_negative_critical_points() {
let at_zero =
PiecewiseIntegral::new(SegmentIntegral::new(1).unwrap(), vec![0.0], true).unwrap();
let f0 = |t: Real| if t < 0.0 { 1.0 } else { 2.0 };
let got = at_zero.integrate(f0, -1.0, 1.0).unwrap();
assert!(close(got, 3.0), "zero breakpoint: got {got}");
let at_neg =
PiecewiseIntegral::new(SegmentIntegral::new(1).unwrap(), vec![-2.0], true).unwrap();
let fn2 = |t: Real| if t < -2.0 { 1.0 } else { 3.0 };
let got = at_neg.integrate(fn2, -3.0, 0.0).unwrap();
assert!(close(got, 7.0), "negative breakpoint: got {got}");
}
#[test]
fn reversed_and_degenerate_outer_limits() {
let piecewise = PiecewiseIntegral::new(
SegmentIntegral::new(1).unwrap(),
vec![1.0, 2.0, 3.0, 4.0, 5.0],
true,
)
.unwrap();
assert_eq!(piecewise.integrate(step, 2.0, 2.0).unwrap(), 0.0);
assert!(close(piecewise.integrate(step, 2.5, 0.0).unwrap(), -4.5));
}
#[test]
fn no_critical_points_delegates_whole_interval() {
let piecewise =
PiecewiseIntegral::new(SegmentIntegral::new(1000).unwrap(), vec![], true).unwrap();
assert!(close(piecewise.integrate(|x| x, 0.0, 2.0).unwrap(), 2.0));
}
#[test]
fn non_finite_critical_points_rejected() {
for bad in [Real::NAN, Real::INFINITY, Real::NEG_INFINITY] {
assert!(
PiecewiseIntegral::new(SegmentIntegral::new(1).unwrap(), vec![1.0, bad], true)
.is_err(),
"critical point {bad} should be rejected"
);
}
}
}