use nalgebra::ComplexField;
use num_traits::{Float, FromPrimitive};
mod error;
mod policy;
mod poly;
mod pre;
mod segment;
pub use policy::SingularityHandling;
pub(crate) use segment::{PathKey, Segment};
pub use segment::{QuadratureSample, QuadratureSamples};
pub use error::IntegratorError;
use crate::{ContourPiece, ErrorNorm, FallibleIntegrable, IntegrationOutput};
pub(crate) struct GaussKronrodConfig<F> {
order: usize,
minimum_segment_width: F,
error_norm: ErrorNorm,
singularity_handling: SingularityHandling,
}
impl<F: FromPrimitive> Default for GaussKronrodConfig<F> {
fn default() -> Self {
Self {
order: 10,
minimum_segment_width: F::from_f64(1e-8).unwrap(),
error_norm: ErrorNorm::Max,
singularity_handling: SingularityHandling::default(),
}
}
}
impl<F> GaussKronrodConfig<F> {
pub(crate) fn new(
order: usize,
minimum_segment_width: F,
error_norm: ErrorNorm,
singularity_handling: SingularityHandling,
) -> Self {
Self {
order,
minimum_segment_width,
error_norm,
singularity_handling,
}
}
}
#[derive(Debug)]
pub struct GaussKronrod<F> {
m: usize,
n: usize,
pub xgk: Vec<F>,
pub wg: Vec<F>,
pub wgk: Vec<F>,
minimum_segment_width: F,
error_norm: ErrorNorm,
singularity_handling: SingularityHandling,
}
impl<F: FromPrimitive> Default for GaussKronrod<F> {
fn default() -> Self {
let config = GaussKronrodConfig::default();
let m = config.order;
let n = m + 1;
Self {
m,
n,
xgk: pre::XGK_10_F64
.into_iter()
.map(|val| F::from_f64(val).unwrap())
.collect::<Vec<_>>(),
wg: pre::WG_10_F64
.into_iter()
.map(|val| F::from_f64(val).unwrap())
.collect::<Vec<_>>(),
wgk: pre::WGK_10_F64
.into_iter()
.map(|val| F::from_f64(val).unwrap())
.collect::<Vec<_>>(),
minimum_segment_width: config.minimum_segment_width,
error_norm: config.error_norm,
singularity_handling: config.singularity_handling,
}
}
}
fn checked_integrand<Y, S>(
integrand: &Y,
input: &Y::Input,
) -> Result<Y::Output, IntegratorError<Y::Input, Y::Error>>
where
Y: FallibleIntegrable,
Y::Output: IntegrationOutput<S, Float = Y::Float>,
{
let value = integrand
.fallible_integrand(input)
.map_err(IntegratorError::User)?;
if !value.is_finite() {
return Err(IntegratorError::NonFiniteIntegrand { point: *input });
}
Ok(value)
}
impl<F> GaussKronrod<F>
where
F: Float + FromPrimitive + std::ops::SubAssign + std::ops::AddAssign,
{
pub fn new(config: GaussKronrodConfig<F>) -> Self {
let m = config.order;
let n = m + 1;
if m == 10 {
Self {
m,
n,
xgk: pre::XGK_10_F64
.into_iter()
.map(|val| F::from_f64(val).unwrap())
.collect::<Vec<_>>(),
wg: pre::WG_10_F64
.into_iter()
.map(|val| F::from_f64(val).unwrap())
.collect::<Vec<_>>(),
wgk: pre::WGK_10_F64
.into_iter()
.map(|val| F::from_f64(val).unwrap())
.collect::<Vec<_>>(),
minimum_segment_width: config.minimum_segment_width,
error_norm: config.error_norm,
singularity_handling: config.singularity_handling,
}
} else {
let zeros = Self::compute_legendre_zeros(m);
let coeffs = Self::compute_chebyshev_coefficients(m);
let abscissae = Self::compute_gauss_kronrod_abscissae(m, &coeffs, &zeros);
let weights = Self::compute_gauss_kronrod_weights(&abscissae, &coeffs);
Self {
m,
n,
xgk: abscissae,
wg: weights.gauss,
wgk: weights.gauss_kronrod,
minimum_segment_width: config.minimum_segment_width,
error_norm: config.error_norm,
singularity_handling: config.singularity_handling,
}
}
}
pub(crate) fn evaluations_per_segment(&self) -> usize {
2 * self.n - 1
}
}
impl<F> GaussKronrod<F> {
pub(crate) fn integrate_piece_with_policy<Y, P>(
&self,
integrand: &Y,
piece: &P,
key: PathKey,
store_segment_data: bool,
) -> Result<Vec<Segment<P, Y::Output, F>>, IntegratorError<Y::Input, Y::Error>>
where
F: Float + FromPrimitive,
Y: FallibleIntegrable<Float = F>,
<Y as FallibleIntegrable>::Output: IntegrationOutput<Y::Input, Float = F>,
P: ContourPiece<Input = Y::Input, Float = F>,
{
match self.singularity_handling {
SingularityHandling::Error => self
.integrate_piece(integrand, piece, key, store_segment_data)
.map(|segment| vec![segment]),
SingularityHandling::RecursiveSplit { max_depth } => self
.integrate_piece_with_singularity_splitting_inner(
integrand,
piece,
key,
store_segment_data,
0,
max_depth,
),
}
}
fn integrate_piece_with_singularity_splitting_inner<Y, P>(
&self,
integrand: &Y,
piece: &P,
key: PathKey,
store_segment_data: bool,
depth: usize,
max_depth: usize,
) -> Result<Vec<Segment<P, Y::Output, F>>, IntegratorError<Y::Input, Y::Error>>
where
F: Float + FromPrimitive,
Y: FallibleIntegrable<Float = F>,
<Y as FallibleIntegrable>::Output: IntegrationOutput<Y::Input, Float = F>,
P: ContourPiece<Input = Y::Input, Float = F>,
{
match self.integrate_piece(integrand, piece, key.clone(), store_segment_data) {
Ok(segment) => Ok(vec![segment]),
Err(IntegratorError::NonFiniteIntegrand { point }) => {
if depth >= max_depth {
return Err(IntegratorError::PossibleSingularity { singularity: point });
}
if piece.length_scale() <= self.minimum_segment_width {
return Err(IntegratorError::PossibleSingularity { singularity: point });
}
let [first_piece, second_piece] = piece.split();
let mut first = self.integrate_piece_with_singularity_splitting_inner(
integrand,
&first_piece,
key.left_child(),
store_segment_data,
depth + 1,
max_depth,
)?;
let second = self.integrate_piece_with_singularity_splitting_inner(
integrand,
&second_piece,
key.right_child(),
store_segment_data,
depth + 1,
max_depth,
)?;
first.extend(second);
Ok(first)
}
Err(error) => Err(error),
}
}
pub(crate) fn integrate_piece<Y, P>(
&self,
integrand: &Y,
piece: &P,
path_key: PathKey,
store_segment_samples: bool,
) -> Result<Segment<P, Y::Output, F>, IntegratorError<Y::Input, Y::Error>>
where
F: Float + FromPrimitive,
Y: FallibleIntegrable<Float = F>,
<Y as FallibleIntegrable>::Output: IntegrationOutput<Y::Input, Float = F>,
P: ContourPiece<Input = Y::Input, Float = F>,
{
if piece.is_degenerate() {
return Err(IntegratorError::EmptySegment);
}
let mut left_samples = if store_segment_samples {
Some(Vec::with_capacity(self.n - 1))
} else {
None
};
let mut right_samples = if store_segment_samples {
Some(Vec::with_capacity(self.n - 1))
} else {
None
};
let two_f = F::one() + F::one();
let half_f = F::one() / two_f;
let half_i = Y::Input::from_real(half_f);
let t_center = half_f;
let point_center = piece.point(t_center);
let jac_center = piece.derivative(t_center);
let f_center = checked_integrand(integrand, &point_center)?;
let center_weight = self.wgk[self.n - 1];
let center_weight_i = Y::Input::from_real(center_weight) * half_i;
let center_physical_weight = jac_center * center_weight_i;
let center_abs_weight = jac_center.modulus() * center_weight * half_f;
let centre_sample = if store_segment_samples {
Some(QuadratureSample {
point: point_center,
weight: center_physical_weight,
value: f_center.clone(),
})
} else {
None
};
let mut result_kronrod = f_center.mul_scalar(¢er_physical_weight);
let mut result_abs = f_center.modulus() * center_abs_weight;
let mut result_gauss = if self.n % 2 == 0 {
let gauss_weight = self.wg[self.n / 2 - 1];
let physical_weight = jac_center * Y::Input::from_real(gauss_weight * half_f);
Some(f_center.mul_scalar(&physical_weight))
} else {
None
};
let mut fv1 = Vec::with_capacity(self.n - 1);
let mut fv2 = Vec::with_capacity(self.n - 1);
let mut points_left = Vec::with_capacity(self.n - 1);
let mut points_right = Vec::with_capacity(self.n - 1);
let mut weights_left = Vec::with_capacity(self.n - 1);
let mut weights_right = Vec::with_capacity(self.n - 1);
for j in 0..self.n - 1 {
let t_left = (F::one() - self.xgk[j]) * half_f;
let t_right = (F::one() + self.xgk[j]) * half_f;
let point_left = piece.point(t_left);
let point_right = piece.point(t_right);
let jac_left = piece.derivative(t_left);
let jac_right = piece.derivative(t_right);
let f_left = checked_integrand(integrand, &point_left)?;
let f_right = checked_integrand(integrand, &point_right)?;
let wk = self.wgk[j];
let weight_left = jac_left * Y::Input::from_real(wk * half_f);
let weight_right = jac_right * Y::Input::from_real(wk * half_f);
result_kronrod = result_kronrod
.add(&f_left.mul_scalar(&weight_left))
.add(&f_right.mul_scalar(&weight_right));
result_abs = result_abs
+ f_left.modulus() * jac_left.modulus() * wk * half_f
+ f_right.modulus() * jac_right.modulus() * wk * half_f;
if j % 2 == 1 {
let gauss_idx = j / 2;
let wg = self.wg[gauss_idx];
let gauss_weight_left = jac_left * Y::Input::from_real(wg * half_f);
let gauss_weight_right = jac_right * Y::Input::from_real(wg * half_f);
let contribution = f_left
.mul_scalar(&gauss_weight_left)
.add(&f_right.mul_scalar(&gauss_weight_right));
result_gauss = Some(match result_gauss {
Some(current) => current.add(&contribution),
None => contribution,
});
}
if let Some(samples) = &mut left_samples {
samples.push(QuadratureSample {
point: point_left,
weight: weight_left,
value: f_left.clone(),
});
}
if let Some(samples) = &mut right_samples {
samples.push(QuadratureSample {
point: point_right,
weight: weight_right,
value: f_right.clone(),
});
}
fv1.push(f_left);
fv2.push(f_right);
points_left.push(point_left);
points_right.push(point_right);
weights_left.push(weight_left);
weights_right.push(weight_right);
}
let mean = result_kronrod.clone();
let mut result_asc = f_center.sub(&mean).modulus() * center_abs_weight;
for j in 0..self.n - 1 {
result_asc = result_asc
+ fv1[j].sub(&mean).modulus() * weights_left[j].modulus()
+ fv2[j].sub(&mean).modulus() * weights_right[j].modulus();
}
let raw_error_output = result_gauss.map_or_else(
|| result_kronrod.clone(),
|result_gauss| result_kronrod.sub(&result_gauss),
);
let raw_error = raw_error_output.reduce_error(self.error_norm);
let error = Self::rescale_error(raw_error, result_abs, result_asc);
Ok(Segment {
piece: piece.clone(),
result: result_kronrod,
error,
key: path_key,
samples: match (left_samples, centre_sample, right_samples) {
(Some(left), Some(centre), Some(right)) => {
Some(QuadratureSamples::from_parts(left, centre, right))
}
_ => None,
},
})
}
pub(crate) fn refine_segment<Y, P>(
&self,
integrand: &Y,
segment: Segment<P, Y::Output, F>,
store_segment_samples: bool,
) -> Result<Vec<Segment<P, Y::Output, F>>, IntegratorError<Y::Input, Y::Error>>
where
F: Float + FromPrimitive,
Y: FallibleIntegrable<Float = F>,
<Y as FallibleIntegrable>::Output: IntegrationOutput<Y::Input, Float = F>,
P: ContourPiece<Input = Y::Input, Float = F>,
{
let [first_piece, second_piece] = segment.piece.split();
if (first_piece.length_scale() < self.minimum_segment_width)
|| (second_piece.length_scale() < self.minimum_segment_width)
{
return Err(IntegratorError::PieceTooSmall);
}
let first = self.integrate_piece_with_policy(
integrand,
&first_piece,
segment.key.left_child(),
store_segment_samples,
)?;
let second = self.integrate_piece_with_policy(
integrand,
&second_piece,
segment.key.right_child(),
store_segment_samples,
)?;
Ok(first.into_iter().chain(second).collect())
}
fn rescale_error(raw_error: F, result_abs: F, result_asc: F) -> F
where
F: Float + FromPrimitive,
{
let zero = F::zero();
let one = F::one();
let fifty = F::from_f64(50.0).unwrap();
let two_hundred = F::from_f64(200.0).unwrap();
let exponent = F::from_f64(1.5).unwrap();
let mut error = raw_error.abs();
if result_asc != zero && error != zero {
let scale = (two_hundred * error / result_asc).powf(exponent);
error = if scale < one {
result_asc * scale
} else {
result_asc
};
}
if result_abs > F::min_positive_value() / (fifty * F::epsilon()) {
let min_error = fifty * F::epsilon() * result_abs;
if min_error > error {
error = min_error;
}
}
error
}
}
#[cfg(test)]
mod integrate_piece_tests {
use super::*;
use crate::{CircularArc, LineSegment};
use nalgebra::Complex;
const TOL: f64 = 1e-10;
fn assert_close(a: f64, b: f64) {
assert!(
(a - b).abs() < TOL,
"expected {b}, got {a}, diff = {}",
(a - b).abs()
);
}
fn assert_complex_close(a: Complex<f64>, b: Complex<f64>) {
assert_close(a.re, b.re);
assert_close(a.im, b.im);
}
struct RealFn<F>(F);
impl<G> FallibleIntegrable for RealFn<G>
where
G: Fn(&f64) -> f64,
{
type Float = f64;
type Input = f64;
type Output = f64;
type Error = std::convert::Infallible;
fn fallible_integrand(&self, x: &f64) -> Result<f64, Self::Error> {
Ok(self.0(x))
}
}
struct ComplexFn<F>(F);
impl<G> FallibleIntegrable for ComplexFn<G>
where
G: Fn(&Complex<f64>) -> Complex<f64>,
{
type Float = f64;
type Input = Complex<f64>;
type Output = Complex<f64>;
type Error = std::convert::Infallible;
fn fallible_integrand(&self, z: &Complex<f64>) -> Result<Complex<f64>, Self::Error> {
Ok(self.0(z))
}
}
#[test]
fn integrates_constant_function_on_real_interval() {
let gk = GaussKronrod::<f64>::default();
let f = RealFn(|_: &f64| 3.0);
let piece: LineSegment<f64> = (2.0..5.0).into();
let segment = gk
.integrate_piece(&f, &piece, PathKey::new(0), false)
.unwrap();
assert_close(segment.result, 9.0);
assert!(segment.error >= 0.0);
assert!(segment.samples.is_none());
}
#[test]
fn integrates_linear_function_on_real_interval() {
let gk = GaussKronrod::<f64>::default();
let f = RealFn(|x: &f64| 2.0 * x + 1.0);
let piece: LineSegment<f64> = (-1.0..3.0).into();
let segment = gk
.integrate_piece(&f, &piece, PathKey::new(0), false)
.unwrap();
assert_close(segment.result, 12.0);
assert!(segment.error >= 0.0);
}
#[test]
fn integrates_quadratic_function_on_real_interval() {
let gk = GaussKronrod::<f64>::default();
let f = RealFn(|x: &f64| x * x);
let piece: LineSegment<f64> = (0.0..2.0).into();
let segment = gk
.integrate_piece(&f, &piece, PathKey::new(0), false)
.unwrap();
assert_close(segment.result, 8.0 / 3.0);
}
#[test]
fn rejects_empty_segment() {
let gk = GaussKronrod::<f64>::default();
let f = RealFn(|x: &f64| *x);
let piece: LineSegment<f64> = (1.0..1.0).into();
let result = gk.integrate_piece(&f, &piece, PathKey::new(0), false);
assert!(matches!(result, Err(IntegratorError::EmptySegment)));
}
#[test]
fn stores_segment_samples_when_requested() {
let gk = GaussKronrod::<f64>::default();
let f = RealFn(|x: &f64| x * x);
let piece: LineSegment<f64> = (0.0..1.0).into();
let segment = gk
.integrate_piece(&f, &piece, PathKey::new(0), true)
.unwrap();
let samples = segment.samples.expect("expected segment samples");
let expected_len = 2 * gk.n - 1;
assert_eq!(samples.len(), expected_len);
}
#[test]
fn omits_segment_samples_when_not_requested() {
let gk = GaussKronrod::<f64>::default();
let f = RealFn(|x: &f64| x * x);
let piece: LineSegment<f64> = (0.0..1.0).into();
let segment = gk
.integrate_piece(&f, &piece, PathKey::new(0), false)
.unwrap();
assert!(segment.samples.is_none());
}
#[test]
fn reports_non_finite_integrand_as_error() {
let gk = GaussKronrod::<f64>::default();
let f = RealFn(|_: &f64| f64::NAN);
let piece: LineSegment<f64> = (0.0..1.0).into();
let result = gk.integrate_piece(&f, &piece, PathKey::new(0), false);
assert!(result.is_err());
}
#[test]
fn integrates_constant_along_complex_contour() {
let gk = GaussKronrod::<f64>::default();
let f = ComplexFn(|_: &Complex<f64>| Complex::new(2.0, 0.0));
let start = Complex::new(0.0, 0.0);
let end = Complex::new(1.0, 1.0);
let piece: LineSegment<Complex<f64>> = (start..end).into();
let segment = gk
.integrate_piece(&f, &piece, PathKey::new(0), false)
.unwrap();
assert_complex_close(segment.result, Complex::new(2.0, 2.0));
}
#[test]
fn integrates_identity_along_complex_contour() {
let gk = GaussKronrod::<f64>::default();
let f = ComplexFn(|z: &Complex<f64>| *z);
let start = Complex::new(0.0, 0.0);
let end = Complex::new(1.0, 1.0);
let piece: LineSegment<Complex<f64>> = (start..end).into();
let segment = gk
.integrate_piece(&f, &piece, PathKey::new(0), false)
.unwrap();
assert_complex_close(segment.result, Complex::new(0.0, 1.0));
}
#[test]
fn refine_segment_splits_range_at_midpoint_for_simple_integrand() {
let gk = GaussKronrod::<f64>::default();
let f = RealFn(|x: &f64| *x);
let piece: LineSegment<f64> = (0.0..4.0).into();
let original = gk
.integrate_piece(&f, &piece, PathKey::new(0), false)
.unwrap();
let segments = gk.refine_segment(&f, original, false).unwrap();
assert_eq!(segments.len(), 2);
let left = &segments[0];
let right = &segments[1];
assert_close(left.piece.point(0.0), 0.0);
assert_close(left.piece.point(1.0), 2.0);
assert_close(right.piece.point(0.0), 2.0);
assert_close(right.piece.point(1.0), 4.0);
}
#[test]
fn refine_segment_preserves_integral_sum_for_polynomial() {
let gk = GaussKronrod::<f64>::default();
let f = RealFn(|x: &f64| x * x + 2.0 * x + 1.0);
let piece: LineSegment<f64> = (-1.0..3.0).into();
let original = gk
.integrate_piece(&f, &piece, PathKey::new(0), false)
.unwrap();
let segments = gk.refine_segment(&f, original.clone(), false).unwrap();
assert_close(
segments.iter().map(|each| each.result).sum(),
original.result,
);
}
#[test]
fn refine_segment_returns_two_non_empty_segments_for_simple_integrand() {
let gk = GaussKronrod::<f64>::default();
let f = RealFn(|x: &f64| x.sin());
let piece: LineSegment<f64> = (-2.0..5.0).into();
let original = gk
.integrate_piece(&f, &piece, PathKey::new(0), false)
.unwrap();
let segments = gk.refine_segment(&f, original, false).unwrap();
assert_eq!(segments.len(), 2);
let left = &segments[0];
let right = &segments[1];
assert!(left.piece.point(0.0) != left.piece.point(1.0));
assert!(right.piece.point(0.0) != right.piece.point(1.0));
assert_eq!(left.piece.point(1.0), right.piece.point(0.0));
}
#[test]
fn refine_segment_works_for_complex_contour() {
let gk = GaussKronrod::<f64>::default();
let f = ComplexFn(|z: &Complex<f64>| *z);
let start = Complex::new(0.0, 0.0);
let end = Complex::new(2.0, 2.0);
let piece: LineSegment<Complex<f64>> = (start..end).into();
let original = gk
.integrate_piece(&f, &piece, PathKey::new(0), false)
.unwrap();
let segments = gk.refine_segment(&f, original.clone(), false).unwrap();
assert_eq!(segments.len(), 2);
let left = &segments[0];
let right = &segments[1];
assert_complex_close(left.piece.point(0.0), start);
assert_complex_close(left.piece.point(1.0), Complex::new(1.0, 1.0));
assert_complex_close(right.piece.point(0.0), Complex::new(1.0, 1.0));
assert_complex_close(right.piece.point(1.0), end);
assert_complex_close(
segments.iter().map(|each| each.result).sum(),
original.result,
);
}
#[test]
fn singularity_policy_error_does_not_split() {
let gk = GaussKronrod::<f64> {
singularity_handling: SingularityHandling::Error,
..Default::default()
};
let f = RealFn(|_: &f64| f64::NAN);
let piece: LineSegment<f64> = (0.0..1.0).into();
let result = gk.integrate_piece_with_policy(&f, &piece, PathKey::new(0), false);
assert!(matches!(
result,
Err(IntegratorError::NonFiniteIntegrand { .. })
));
}
#[test]
fn recursive_singularity_splitting_succeeds_for_endpoint_singularity() {
let gk = GaussKronrod::<f64> {
singularity_handling: SingularityHandling::RecursiveSplit { max_depth: 8 },
minimum_segment_width: 1e-14,
..Default::default()
};
let f = RealFn(|x: &f64| if *x == 0.0 { f64::NAN } else { x.sqrt() });
let piece: LineSegment<f64> = (0.0..1.0).into();
let segments = gk
.integrate_piece_with_policy(&f, &piece, PathKey::new(0), false)
.unwrap();
assert!(!segments.is_empty());
for segment in segments {
assert!(segment.result.is_finite());
assert!(segment.error.is_finite());
}
}
#[test]
fn recursive_singularity_splitting_handles_bad_midpoint() {
let gk = GaussKronrod::<f64> {
singularity_handling: SingularityHandling::RecursiveSplit { max_depth: 8 },
minimum_segment_width: 1e-14,
..Default::default()
};
let f = RealFn(|x: &f64| {
if (*x - 0.5).abs() < 1e-15 {
f64::NAN
} else {
1.0
}
});
let piece: LineSegment<f64> = (0.0..1.0).into();
let segments = gk
.integrate_piece_with_policy(&f, &piece, PathKey::new(0), false)
.unwrap();
assert_eq!(segments.len(), 2);
let total: f64 = segments.iter().map(|segment| segment.result).sum();
assert_close(total, 1.0);
}
#[test]
fn recursive_singularity_splitting_respects_max_depth() {
let gk = GaussKronrod::<f64> {
singularity_handling: SingularityHandling::RecursiveSplit { max_depth: 0 },
minimum_segment_width: 1e-14,
..Default::default()
};
let f = RealFn(|x: &f64| {
if (*x - 0.5).abs() < 1e-15 {
f64::NAN
} else {
1.0
}
});
let piece: LineSegment<f64> = (0.0..1.0).into();
let result = gk.integrate_piece_with_policy(&f, &piece, PathKey::new(0), false);
assert!(matches!(
result,
Err(IntegratorError::PossibleSingularity { .. })
));
}
#[test]
fn integrates_constant_over_circular_arc() {
let gk = GaussKronrod::<f64>::default();
let f = ComplexFn(|_: &Complex<f64>| Complex::new(1.0, 0.0));
let piece = CircularArc::new(
Complex::new(0.0, 0.0),
1.0,
0.0,
std::f64::consts::FRAC_PI_2,
);
let segment = gk
.integrate_piece(&f, &piece, PathKey::new(0), false)
.unwrap();
assert_complex_close(segment.result, Complex::new(-1.0, 1.0));
}
#[test]
fn integrates_inverse_z_over_unit_semicircle() {
let gk = GaussKronrod::<f64>::default();
let f = ComplexFn(|z: &Complex<f64>| Complex::new(1.0, 0.0) / *z);
let piece = CircularArc::new(Complex::new(0.0, 0.0), 1.0, 0.0, std::f64::consts::PI);
let segment = gk
.integrate_piece(&f, &piece, PathKey::new(0), false)
.unwrap();
assert_complex_close(segment.result, Complex::new(0.0, std::f64::consts::PI));
}
}