use crate::error::{IntegrateError, IntegrateResult};
use std::f64::consts::PI;
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub enum ContourType {
#[default]
Real,
SemiCircle {
radius: f64,
},
Talbot {
sigma: f64,
nu: f64,
},
IndentedReal {
indent_radius: f64,
indent_at: Vec<f64>,
},
}
#[derive(Debug, Clone)]
pub struct ContourConfig {
pub n_initial: usize,
pub max_levels: usize,
pub tol: f64,
pub contour_type: ContourType,
}
impl Default for ContourConfig {
fn default() -> Self {
Self {
n_initial: 16,
max_levels: 8,
tol: 1e-10,
contour_type: ContourType::Real,
}
}
}
#[derive(Debug, Clone)]
pub struct ContourResult {
pub value: f64,
pub error: f64,
pub n_evaluations: usize,
pub converged: bool,
}
pub fn talbot_contour(sigma: f64, nu: f64, theta: f64) -> (f64, f64) {
let re = if theta.abs() < 1e-8 {
sigma + nu * (-theta * theta / 3.0)
} else {
sigma + nu * (theta / theta.tan() - 1.0)
};
let im = nu * nu * theta;
(re, im)
}
fn talbot_contour_deriv(nu: f64, theta: f64) -> (f64, f64) {
let d_re = if theta.abs() < 1e-8 {
nu * (-2.0 * theta / 3.0)
} else {
let s = theta.sin();
nu * (theta.cos() / s - theta / (s * s))
};
let d_im = nu * nu;
(d_re, d_im)
}
pub fn semicircle_contour(r: f64, theta: f64) -> (f64, f64) {
(r * theta.cos(), r * theta.sin())
}
fn semicircle_contour_deriv(r: f64, theta: f64) -> (f64, f64) {
(-r * theta.sin(), r * theta.cos())
}
fn cc_nodes_weights(n: usize) -> (Vec<f64>, Vec<f64>) {
if n == 1 {
return (vec![0.0], vec![2.0]);
}
let m = n - 1;
let mf = m as f64;
let nodes: Vec<f64> = (0..n).map(|j| -(PI * j as f64 / mf).cos()).collect();
let mut weights = vec![0.0_f64; n];
for j in 0..n {
let cj = if j == 0 || j == m { 1.0 } else { 2.0 };
let half = m / 2;
let mut s = 0.0_f64;
for k in 1..=half {
let bk = if k == half && m.is_multiple_of(2) {
1.0
} else {
2.0
};
let denom = 4.0 * (k as f64).powi(2) - 1.0;
s += bk / denom * (2.0 * PI * j as f64 * k as f64 / mf).cos();
}
weights[j] = cj / mf * (1.0 - s);
}
(nodes, weights)
}
fn cc_nodes_weights_interval(n: usize, a: f64, b: f64) -> (Vec<f64>, Vec<f64>) {
let (t, wt) = cc_nodes_weights(n);
let mid = 0.5 * (a + b);
let half = 0.5 * (b - a);
let x: Vec<f64> = t.iter().map(|&ti| mid + half * ti).collect();
let w: Vec<f64> = wt.iter().map(|&wi| half * wi).collect();
(x, w)
}
fn cc_error_estimate(f: &impl Fn(f64) -> f64, a: f64, b: f64, n: usize) -> (f64, f64, usize) {
let n_coarse = (n / 2 + 1).max(2);
let (x_fine, w_fine) = cc_nodes_weights_interval(n, a, b);
let (x_coarse, w_coarse) = cc_nodes_weights_interval(n_coarse, a, b);
let val_fine: f64 = x_fine
.iter()
.zip(w_fine.iter())
.map(|(&xi, &wi)| wi * f(xi))
.sum();
let val_coarse: f64 = x_coarse
.iter()
.zip(w_coarse.iter())
.map(|(&xi, &wi)| wi * f(xi))
.sum();
let error = (val_fine - val_coarse).abs();
let n_evals = n + n_coarse;
(val_fine, error, n_evals)
}
pub fn adaptive_cc(
f: impl Fn(f64) -> f64,
a: f64,
b: f64,
config: &ContourConfig,
) -> IntegrateResult<ContourResult> {
if a >= b {
return Err(IntegrateError::InvalidInput(format!(
"adaptive_cc: a must be < b, got [{a}, {b}]"
)));
}
if config.n_initial < 2 {
return Err(IntegrateError::InvalidInput(
"n_initial must be >= 2".into(),
));
}
let mut intervals: Vec<(f64, f64, f64, f64)> = Vec::new(); let mut total_evals = 0_usize;
let (val0, err0, ne0) = cc_error_estimate(&f, a, b, config.n_initial);
total_evals += ne0;
intervals.push((a, b, val0, err0));
let mut converged = false;
for _level in 0..config.max_levels {
let global_error: f64 = intervals.iter().map(|&(_, _, _, e)| e).sum();
if global_error <= config.tol {
converged = true;
break;
}
let worst_idx = intervals
.iter()
.enumerate()
.max_by(|a, b| {
a.1 .3
.partial_cmp(&b.1 .3)
.unwrap_or(std::cmp::Ordering::Equal)
})
.map(|(i, _)| i)
.unwrap_or(0);
let (ai, bi, _, _) = intervals.remove(worst_idx);
let mid = 0.5 * (ai + bi);
let (val_l, err_l, ne_l) = cc_error_estimate(&f, ai, mid, config.n_initial);
let (val_r, err_r, ne_r) = cc_error_estimate(&f, mid, bi, config.n_initial);
total_evals += ne_l + ne_r;
intervals.push((ai, mid, val_l, err_l));
intervals.push((mid, bi, val_r, err_r));
}
let value: f64 = intervals.iter().map(|&(_, _, v, _)| v).sum();
let error: f64 = intervals.iter().map(|&(_, _, _, e)| e).sum();
Ok(ContourResult {
value,
error,
n_evaluations: total_evals,
converged,
})
}
fn cc_complex_on_arc(
f: &impl Fn(f64, f64) -> (f64, f64),
z_fn: impl Fn(f64) -> (f64, f64),
dz_fn: impl Fn(f64) -> (f64, f64),
t_start: f64,
t_end: f64,
n_pts: usize,
) -> (f64, f64) {
let (t_nodes, t_weights) = cc_nodes_weights_interval(n_pts, t_start, t_end);
let mut re_sum = 0.0_f64;
let mut im_sum = 0.0_f64;
for (&t, &wt) in t_nodes.iter().zip(t_weights.iter()) {
let (zr, zi) = z_fn(t);
let (dzr, dzi) = dz_fn(t);
let (fr, fi) = f(zr, zi);
let integrand_r = fr * dzr - fi * dzi;
let integrand_i = fr * dzi + fi * dzr;
re_sum += wt * integrand_r;
im_sum += wt * integrand_i;
}
(re_sum, im_sum)
}
pub fn contour_integrate_cc(
f: impl Fn(f64, f64) -> (f64, f64),
contour: &ContourType,
n_pts: usize,
) -> IntegrateResult<(f64, f64)> {
if n_pts < 2 {
return Err(IntegrateError::InvalidInput("n_pts must be >= 2".into()));
}
match contour {
ContourType::Real => {
Err(IntegrateError::InvalidInput(
"For Real contour use adaptive_cc instead".into(),
))
}
ContourType::SemiCircle { radius } => {
let r = *radius;
let z_fn = move |t: f64| semicircle_contour(r, t);
let dz_fn = move |t: f64| semicircle_contour_deriv(r, t);
let (re, im) = cc_complex_on_arc(&f, z_fn, dz_fn, -PI, PI, n_pts);
Ok((re, im))
}
ContourType::Talbot { sigma, nu } => {
let s = *sigma;
let v = *nu;
let z_fn = move |t: f64| talbot_contour(s, v, t);
let dz_fn = move |t: f64| talbot_contour_deriv(v, t);
let t_start = -PI * (1.0 - 1e-10);
let t_end = PI * (1.0 - 1e-10);
let (re, im) = cc_complex_on_arc(&f, z_fn, dz_fn, t_start, t_end, n_pts);
Ok((re, im))
}
ContourType::IndentedReal {
indent_radius,
indent_at,
} => {
let r = *indent_radius;
let poles = indent_at.clone();
if poles.is_empty() {
Err(IntegrateError::InvalidInput(
"IndentedReal requires at least one indent_at point".into(),
))
} else {
let mut sorted_poles = poles.clone();
sorted_poles.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let x_min = sorted_poles[0] - 10.0 * r;
let x_max = sorted_poles[sorted_poles.len() - 1] + 10.0 * r;
let mut re_total = 0.0_f64;
let mut im_total = 0.0_f64;
let mut prev = x_min;
for &pole in &sorted_poles {
let seg_end = pole - r;
if prev < seg_end {
let z_fn = |t: f64| (t, 0.0_f64);
let dz_fn = |_: f64| (1.0_f64, 0.0_f64);
let (re, im) = cc_complex_on_arc(&f, z_fn, dz_fn, prev, seg_end, n_pts);
re_total += re;
im_total += im;
}
let z_fn = {
let p = pole;
move |t: f64| (p + r * t.cos(), r * t.sin())
};
let dz_fn = move |t: f64| (-r * t.sin(), r * t.cos());
let (re_ind, im_ind) = cc_complex_on_arc(&f, z_fn, dz_fn, 0.0, PI, n_pts);
re_total += re_ind;
im_total += im_ind;
prev = pole + r;
}
if prev < x_max {
let z_fn = |t: f64| (t, 0.0_f64);
let dz_fn = |_: f64| (1.0_f64, 0.0_f64);
let (re, im) = cc_complex_on_arc(&f, z_fn, dz_fn, prev, x_max, n_pts);
re_total += re;
im_total += im;
}
Ok((re_total, im_total))
}
}
}
}
pub fn filon_cc_oscillatory(f: impl Fn(f64) -> f64, omega: f64, a: f64, b: f64, n: usize) -> f64 {
let (nodes, weights) = cc_nodes_weights_interval(n, a, b);
nodes
.iter()
.zip(weights.iter())
.map(|(&xi, &wi)| wi * f(xi) * (omega * xi).cos())
.sum()
}
pub fn filon_cc_oscillatory_sin(
f: impl Fn(f64) -> f64,
omega: f64,
a: f64,
b: f64,
n: usize,
) -> f64 {
let (nodes, weights) = cc_nodes_weights_interval(n, a, b);
nodes
.iter()
.zip(weights.iter())
.map(|(&xi, &wi)| wi * f(xi) * (omega * xi).sin())
.sum()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_adaptive_cc_polynomial() {
let cfg = ContourConfig {
tol: 1e-12,
..Default::default()
};
let result = adaptive_cc(|x| x * x, 0.0, 1.0, &cfg).expect("adaptive_cc should succeed");
assert!(
(result.value - 1.0 / 3.0).abs() < 1e-10,
"value = {}, expected = {}",
result.value,
1.0 / 3.0
);
}
#[test]
fn test_adaptive_cc_arctan() {
let cfg = ContourConfig {
tol: 1e-12,
..Default::default()
};
let result = adaptive_cc(|x| 1.0 / (1.0 + x * x), 0.0, 1.0, &cfg)
.expect("adaptive_cc should succeed");
let exact = std::f64::consts::FRAC_PI_4;
assert!(
(result.value - exact).abs() < 1e-8,
"value = {}, exact = {}",
result.value,
exact
);
}
#[test]
fn test_contour_integrate_constant_on_semicircle() {
let result = contour_integrate_cc(
|_re, _im| (1.0, 0.0),
&ContourType::SemiCircle { radius: 1.0 },
32,
)
.expect("contour integral should succeed");
assert!(
result.0.abs() < 1e-10 && result.1.abs() < 1e-10,
"expected ≈ 0, got ({}, {})",
result.0,
result.1
);
}
#[test]
fn test_contour_integrate_z_semicircle() {
let result = contour_integrate_cc(
|re, im| {
(re, im)
},
&ContourType::SemiCircle { radius: 1.0 },
64,
)
.expect("contour integral should succeed");
assert!(
result.0.abs() < 1e-8 && result.1.abs() < 1e-8,
"expected ≈ 0 for ∫z dz on circle, got ({}, {})",
result.0,
result.1
);
}
#[test]
fn test_filon_cc_oscillatory_riemann_lebesgue() {
let val = filon_cc_oscillatory(|x: f64| x.sin(), 10.0, 0.0, PI, 64);
let exact = -2.0_f64 / 99.0;
assert!(
(val - exact).abs() < 1e-6,
"Filon-CC: value = {}, exact = {}",
val,
exact
);
}
#[test]
fn test_adaptive_cc_converged_flag() {
let cfg = ContourConfig {
tol: 1e-8,
max_levels: 10,
..Default::default()
};
let result =
adaptive_cc(|x: f64| x.exp(), 0.0, 1.0, &cfg).expect("adaptive_cc should succeed");
assert!(result.converged, "should converge for smooth function");
}
#[test]
fn test_talbot_contour_values() {
let (re, im) = talbot_contour(1.0, 0.6, 0.0);
assert!((re - 1.0).abs() < 1e-12, "re = {}", re);
assert!(im.abs() < 1e-12, "im = {}", im);
}
}