quad_rs/contour/mod.rs
1//! Parameterised integration contours.
2//!
3//! This module provides the geometric representation used by the adaptive
4//! quadrature routines to integrate over real and complex domains.
5//!
6//! Rather than integrating directly over intervals, the integrator operates on
7//! **contours** composed of one or more parameterised contour pieces. Each
8//! contour piece maps the unit interval `[0, 1]` onto a section of the
9//! integration path and supplies the derivative required by the change of
10//! variables.
11//!
12//! # Mathematical formulation
13//!
14//! Given a contour
15//!
16//! ```text
17//! γ : [0,1] → ℂ,
18//! ```
19//!
20//! the quadrature routines evaluate
21//!
22//! ```text
23//! ∫γ f(z) dz
24//! = ∫₀¹ f(γ(t)) γ'(t) dt.
25//! ```
26//!
27//! The adaptive algorithm therefore operates entirely on the parameter `t`,
28//! while each contour piece is responsible for mapping quadrature nodes into
29//! the physical integration domain.
30//!
31//! This representation naturally supports:
32//!
33//! - real intervals,
34//! - complex line segments,
35//! - circular arcs,
36//! - piecewise contours,
37//! - contours with local deformations,
38//! - user-defined parameterisations.
39//!
40//! # Built-in contour pieces
41//!
42//! The library currently provides:
43//!
44//! - [`LineSegment`] for straight-line paths,
45//! - [`CircularArc`] for circular arcs,
46//! - [`ContourSegment`] for heterogeneous piecewise contours.
47//!
48//! Additional contour pieces can be introduced by implementing
49//! [`ContourPiece`].
50//!
51//! # Contour construction
52//!
53//! A [`Contour`] is simply an ordered collection of contour pieces.
54//!
55//! Convenience constructors are provided for many commonly occurring contours
56//! in applied mathematics and physics, including:
57//!
58//! - finite real intervals,
59//! - shifted real axes,
60//! - upper and lower half-disks,
61//! - offset half-disks,
62//! - piecewise linear contours.
63//!
64//! Contours may also be modified after construction using methods such as
65//! [`Contour::reverse`], [`Contour::close`], and
66//! [`Contour::indent`].
67//!
68//! # Singularity handling
69//!
70//! One of the principal motivations for the contour abstraction is the ability
71//! to deform integration paths without modifying the quadrature algorithm.
72//!
73//! Local deformations can be introduced around known poles using
74//! [`Contour::indent`], replacing a section of a line segment with a small
75//! circular arc. This is useful when evaluating Cauchy principal values,
76//! Green's function integrals, residue calculations, and other contour
77//! integrals involving isolated singularities.
78//!
79//! More sophisticated contour deformations can be built by composing contour
80//! pieces.
81//!
82//! # Infinite domains
83//!
84//! The contour constructors represent finite paths.
85//!
86//! Infinite-domain integrals are typically approximated by increasing the size
87//! of a finite contour until convergence is achieved. Future versions of the
88//! library may provide parameterised contour pieces representing infinite
89//! intervals via suitable coordinate transformations.
90//!
91//! # Examples
92//!
93//! Construct a finite interval along the real axis:
94//!
95//! ```
96//! # use quad_rs::Contour;
97//! let contour = Contour::real_axis(5.0);
98//! ```
99//!
100//! Construct a contour implementing an `i0⁺` prescription:
101//!
102//! ```
103//! # use quad_rs::Contour;
104//! let contour = Contour::real_axis_offset(10.0, 1e-3);
105//! ```
106//!
107//! Construct a closed contour for residue calculations:
108//!
109//! ```
110//! # use quad_rs::Contour;
111//! let contour = Contour::upper_half_disk(20.0);
112//! ```
113//!
114//! Deform a contour around a pole:
115//!
116//! ```
117//! # use num_complex::Complex;
118//! # use quad_rs::{Contour, IndentSide};
119//! let contour = Contour::real_axis(5.0)
120//! .indent(
121//! Complex::new(1.0, 0.0),
122//! 0.1,
123//! IndentSide::Left,
124//! 1e-10,
125//! );
126//! ```
127mod deform;
128mod piece;
129
130pub use deform::IndentSide;
131
132pub(crate) use piece::ContourPiece;
133
134pub use piece::{CircularArc, ContourSegment, LineSegment};
135
136use crate::integrable::ComplexScalar;
137
138use num_traits::FromPrimitive;
139
140/// Ordered integration contour.
141///
142/// A `Contour` stores a sequence of [`ContourSegment`]s. The integrator
143/// evaluates each segment independently and sums the resulting contributions.
144///
145/// The current concrete contour type is designed for complex-valued contours
146/// and supports heterogeneous built-in pieces such as line segments and
147/// circular arcs.
148#[derive(Clone, Debug)]
149pub struct Contour<F: ComplexScalar> {
150 pieces: Vec<ContourSegment<F>>,
151}
152
153impl<F: ComplexScalar> Contour<F> {
154 /// Creates a contour from explicit contour pieces.
155 ///
156 /// # Panics
157 ///
158 /// Panics if `pieces` is empty.
159 pub fn from_pieces(pieces: Vec<ContourSegment<F>>) -> Self {
160 assert!(!pieces.is_empty());
161 Self { pieces }
162 }
163
164 pub fn pieces(&self) -> &[ContourSegment<F>] {
165 &self.pieces
166 }
167
168 pub fn into_pieces(self) -> Vec<ContourSegment<F>> {
169 self.pieces
170 }
171
172 pub fn reverse(mut self) -> Self {
173 self.pieces.reverse();
174
175 self.pieces = self
176 .pieces
177 .into_iter()
178 .map(ContourSegment::reversed)
179 .collect();
180
181 self
182 }
183
184 pub fn close(mut self) -> Self {
185 let Some(first) = self.pieces.first().map(ContourSegment::start) else {
186 return self;
187 };
188
189 let Some(last) = self.pieces.last().map(ContourSegment::end) else {
190 return self;
191 };
192
193 if first != last {
194 self.pieces
195 .push(ContourSegment::Line(LineSegment::new(last, first)));
196 }
197
198 self
199 }
200
201 pub fn with_principal_value(
202 self,
203 pole: F::Complex,
204 radius: F,
205 side: IndentSide,
206 tolerance: F,
207 ) -> Self
208 where
209 F: ComplexScalar + FromPrimitive,
210 {
211 self.indent(pole, radius, side, tolerance)
212 }
213
214 pub fn indent_many(
215 mut self,
216 singularities: impl IntoIterator<Item = (F::Complex, F, IndentSide)>,
217 tolerance: F,
218 ) -> Self
219 where
220 F: ComplexScalar + FromPrimitive,
221 {
222 for (pole, radius, side) in singularities {
223 self = self.indent(pole, radius, side, tolerance);
224 }
225
226 self
227 }
228}
229
230impl<F> Contour<F>
231where
232 F: ComplexScalar,
233{
234 /// Creates a piecewise-linear contour through the supplied points.
235 ///
236 /// Consecutive points are joined by line segments.
237 ///
238 /// # Panics
239 ///
240 /// Panics if fewer than two points are supplied.
241 pub fn piecewise_linear(points: Vec<F::Complex>) -> Self {
242 assert!(points.len() >= 2);
243
244 let pieces = points
245 .windows(2)
246 .map(|pair| ContourSegment::Line(LineSegment::from(pair[0]..pair[1])))
247 .collect();
248
249 Self { pieces }
250 }
251
252 /// Counter-clockwise upper half-disk contour.
253 ///
254 /// Path:
255 ///
256 /// ```text
257 /// center - R → center + R
258 /// center + R → center - R through the upper half-plane
259 /// ```
260 pub fn upper_half_disk_centered(center: F::Complex, radius: F) -> Self
261 where
262 F: FromPrimitive,
263 {
264 let left = center - F::complex(radius, F::zero());
265 let right = center + F::complex(radius, F::zero());
266
267 Self::from_pieces(vec![
268 ContourSegment::Line(LineSegment::new(left, right)),
269 ContourSegment::CircularArc(CircularArc::new(
270 center,
271 radius,
272 F::zero(),
273 F::from_f64(std::f64::consts::PI).unwrap(),
274 )),
275 ])
276 }
277
278 /// Clockwise lower half-disk contour.
279 ///
280 /// Path:
281 ///
282 /// ```text
283 /// center - R → center + R
284 /// center + R → center - R through the lower half-plane
285 /// ```
286 pub fn lower_half_disk_centered(center: F::Complex, radius: F) -> Self
287 where
288 F: FromPrimitive,
289 {
290 let left = center - F::complex(radius, F::zero());
291 let right = center + F::complex(radius, F::zero());
292
293 Self::from_pieces(vec![
294 ContourSegment::Line(LineSegment::new(left, right)),
295 ContourSegment::CircularArc(CircularArc::new(
296 center,
297 radius,
298 F::zero(),
299 -F::from_f64(std::f64::consts::PI).unwrap(),
300 )),
301 ])
302 }
303
304 /// Constructs a contour following the real axis from `-radius` to `radius`.
305 ///
306 /// This is the canonical finite approximation to the real line used when
307 /// numerically evaluating improper integrals over `(-∞, ∞)`.
308 ///
309 /// The contour consists of a single straight line segment.
310 ///
311 /// # Orientation
312 ///
313 /// The contour is traversed from left to right.
314 ///
315 /// # Notes
316 ///
317 /// The infinite real axis is recovered in the limit `radius → ∞`.
318 pub fn real_axis(radius: F) -> Self
319 where
320 F: FromPrimitive,
321 {
322 Self::piecewise_linear(vec![
323 F::complex(-radius, F::zero()),
324 F::complex(radius, F::zero()),
325 ])
326 }
327
328 /// Constructs a straight contour parallel to the real axis.
329 ///
330 /// The contour runs from
331 ///
332 /// ```text
333 /// -radius + i·offset
334 /// ```
335 ///
336 /// to
337 ///
338 /// ```text
339 /// radius + i·offset.
340 /// ```
341 ///
342 /// # Orientation
343 ///
344 /// The contour is traversed from left to right.
345 ///
346 /// # Applications
347 ///
348 /// Offset contours occur frequently in physics and applied mathematics,
349 /// including:
350 ///
351 /// - causal Green's functions (`i0⁺` prescriptions),
352 /// - Laplace and Fourier inversion,
353 /// - contour deformation to avoid poles,
354 /// - regularisation of principal-value integrals.
355 pub fn real_axis_offset(radius: F, imaginary_offset: F) -> Self
356 where
357 F: FromPrimitive,
358 {
359 Self::piecewise_linear(vec![
360 F::complex(-radius, imaginary_offset),
361 F::complex(radius, imaginary_offset),
362 ])
363 }
364
365 /// Constructs a counter-clockwise upper half-disk.
366 ///
367 /// The contour consists of
368 ///
369 /// 1. a straight line along the real axis from `-radius` to `radius`,
370 /// 2. a circular arc returning through the upper half-plane.
371 ///
372 /// ```text
373 /// ●
374 /// .-' '-.
375 /// .' '.
376 /// -R-----------R
377 /// ```
378 ///
379 /// # Orientation
380 ///
381 /// The resulting contour is positively oriented (counter-clockwise).
382 ///
383 /// # Applications
384 ///
385 /// This contour is commonly used with:
386 ///
387 /// - the residue theorem,
388 /// - Jordan's lemma,
389 /// - Fourier transform evaluation,
390 /// - contour integration in wave propagation.
391 ///
392 /// # Notes
393 ///
394 /// The infinite upper-half-plane contour is recovered by taking
395 /// `radius → ∞`.
396 pub fn upper_half_disk(radius: F) -> Self
397 where
398 F: FromPrimitive,
399 {
400 Self::upper_half_disk_centered(F::complex(F::zero(), F::zero()), radius)
401 }
402
403 /// Constructs a clockwise lower half-disk.
404 ///
405 /// The contour consists of
406 ///
407 /// 1. a straight line along the real axis from `-radius` to `radius`,
408 /// 2. a circular arc returning through the lower half-plane.
409 ///
410 /// ```text
411 /// -R-----------R
412 /// '. .'
413 /// '-._.-'
414 /// ```
415 ///
416 /// # Orientation
417 ///
418 /// The resulting contour is negatively oriented (clockwise).
419 ///
420 /// # Applications
421 ///
422 /// Useful when applying the residue theorem to integrands that decay in the
423 /// lower half-plane, such as Fourier integrals with negative arguments.
424 ///
425 /// # Notes
426 ///
427 /// The infinite lower-half-plane contour is recovered by taking
428 /// `radius → ∞`.
429 pub fn lower_half_disk(radius: F) -> Self
430 where
431 F: FromPrimitive,
432 {
433 Self::lower_half_disk_centered(F::complex(F::zero(), F::zero()), radius)
434 }
435
436 /// Constructs an upper half-disk translated vertically.
437 ///
438 /// The contour is identical to [`upper_half_disk`](Self::upper_half_disk)
439 /// except that it is centred at
440 ///
441 /// ```text
442 /// i · imaginary_offset.
443 /// ```
444 ///
445 /// Consequently, the straight segment lies along
446 ///
447 /// ```text
448 /// Im(z) = imaginary_offset.
449 /// ```
450 ///
451 /// # Applications
452 ///
453 /// Shifted contours are useful when implementing
454 ///
455 /// - `i0⁺` prescriptions,
456 /// - contour regularisation,
457 /// - displaced Bromwich contours,
458 /// - Green's function calculations.
459 pub fn upper_half_disk_offset(radius: F, imaginary_offset: F) -> Self
460 where
461 F: FromPrimitive,
462 {
463 Self::upper_half_disk_centered(F::complex(F::zero(), imaginary_offset), radius)
464 }
465
466 /// Constructs a lower half-disk translated vertically.
467 ///
468 /// The contour is identical to [`lower_half_disk`](Self::lower_half_disk)
469 /// except that it is centred at
470 ///
471 /// ```text
472 /// i · imaginary_offset.
473 /// ```
474 ///
475 /// The straight segment therefore lies on
476 ///
477 /// ```text
478 /// Im(z) = imaginary_offset.
479 /// ```
480 ///
481 /// This contour is frequently used when closing contours below the real axis
482 /// while avoiding nearby singularities.
483 pub fn lower_half_disk_offset(radius: F, imaginary_offset: F) -> Self
484 where
485 F: FromPrimitive,
486 {
487 Self::lower_half_disk_centered(F::complex(F::zero(), imaginary_offset), radius)
488 }
489}