Skip to main content

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}