quad_rs/
contour.rs

1use approx::relative_eq;
2use nalgebra::ComplexField;
3
4use num_traits::FromPrimitive;
5
6#[derive(Clone, Debug)]
7/// A contour, which is just a collection of ranges whose
8/// start and end-point overlap
9pub struct Contour<I> {
10    /// The colletion of individual ranges describing the contour
11    pub range: Vec<std::ops::Range<I>>,
12}
13
14/// Defines the direction of the contour in the complex plane
15#[derive(Copy, Clone, Debug)]
16pub enum Direction {
17    /// Clockwise contour
18    Clockwise,
19    /// Anti clockwise contour
20    CounterClockwise,
21}
22
23impl<I> Contour<I> {
24    pub fn reversed(self) -> Self {
25        Self {
26            range: self.range.into_iter().rev().collect(),
27        }
28    }
29}
30
31impl<I> Contour<I>
32where
33    I: ComplexField + FromPrimitive + Copy,
34    <I as ComplexField>::RealField: Copy,
35{
36    /// Generate a closed rectangular contour
37    pub fn generate_rectangular(
38        x_range: &std::ops::Range<I::RealField>,
39        y_range: &std::ops::Range<I::RealField>,
40        direction: Direction,
41    ) -> Self {
42        let mut range = Self::generate_rectangular_internal(x_range, y_range);
43        match direction {
44            Direction::Clockwise => Self { range },
45            Direction::CounterClockwise => {
46                range.reverse();
47                Self { range }
48            }
49        }
50    }
51
52    /// A hacky function to generate the rectangular contour,
53    pub fn generate_rectangular_internal(
54        x_range: &std::ops::Range<I::RealField>,
55        y_range: &std::ops::Range<I::RealField>,
56    ) -> Vec<std::ops::Range<I>> {
57        // let i: I = nalgebra::Complex::new(I::zero().real(), I::one().real()).into();
58        let i = -<I as ComplexField>::try_sqrt(-I::one()).unwrap();
59        let path = [
60            I::from_real(x_range.start) + i * I::from_real(y_range.start),
61            I::from_real(x_range.end) + i * I::from_real(y_range.start),
62            I::from_real(x_range.end) + i * I::from_real(y_range.end),
63            I::from_real(x_range.start) + i * I::from_real(y_range.end),
64            I::from_real(x_range.start) + i * I::from_real(y_range.start),
65        ];
66        let connected_range = path
67            .windows(2)
68            .map(|points| std::ops::Range {
69                start: points[0],
70                end: points[1],
71            })
72            .collect::<Vec<_>>();
73
74        connected_range
75    }
76
77    /// If we detect a singularity on the contour this deforms the contour outward to enclose it
78    pub fn deform_outward_around_singularity(&mut self, singularity: I) {
79        let delta = <I as ComplexField>::RealField::from_f64(1e-5).unwrap();
80        let mut x_range: std::ops::Range<<I as ComplexField>::RealField> = std::ops::Range {
81            start: self.range[0].start.real(),
82            end: self.range[0].end.real(),
83        };
84        let mut y_range: std::ops::Range<<I as ComplexField>::RealField> = std::ops::Range {
85            start: self.range[1].start.imaginary(),
86            end: self.range[1].end.imaginary(),
87        };
88
89        if relative_eq!(singularity.real(), x_range.start) {
90            x_range.start -= delta;
91        } else if relative_eq!(singularity.real(), x_range.end) {
92            x_range.end += delta;
93        } else if relative_eq!(singularity.imaginary(), y_range.start) {
94            y_range.start -= delta;
95        } else if relative_eq!(singularity.imaginary(), y_range.end) {
96            y_range.end += delta;
97        }
98
99        self.range = Self::generate_rectangular_internal(&x_range, &y_range);
100    }
101
102    /// If we detect a singularity on the contour this deforms the contour inward to exclude it
103    pub fn deform_inward_around_singularity(&mut self, singularity: I) {
104        let delta = <I as ComplexField>::RealField::from_f64(-1e-5).unwrap();
105        let mut x_range: std::ops::Range<<I as ComplexField>::RealField> = std::ops::Range {
106            start: self.range[0].start.real(),
107            end: self.range[0].end.real(),
108        };
109        let mut y_range: std::ops::Range<<I as ComplexField>::RealField> = std::ops::Range {
110            start: self.range[1].start.imaginary(),
111            end: self.range[1].end.imaginary(),
112        };
113
114        if relative_eq!(singularity.real(), x_range.start) {
115            x_range.start -= delta;
116        } else if relative_eq!(singularity.real(), x_range.end) {
117            x_range.end += delta;
118        } else if relative_eq!(singularity.imaginary(), y_range.start) {
119            y_range.start -= delta;
120        } else if relative_eq!(singularity.imaginary(), y_range.end) {
121            y_range.end += delta;
122        }
123
124        self.range = Self::generate_rectangular_internal(&x_range, &y_range);
125    }
126}
127
128/// This splits a range around a given set of singularities
129pub fn split_range_once_around_singularity<I>(
130    range: std::ops::Range<I>,
131    singularity: I,
132) -> [std::ops::Range<I>; 2]
133where
134    I: ComplexField + Copy,
135{
136    let ranges = split_range_around_singularities(range, vec![singularity]);
137    [ranges[0].clone(), ranges[1].clone()] // TODO: Stop this...
138}
139
140/// This splits a range around a given set of singularities
141pub fn split_range_around_singularities<I>(
142    range: std::ops::Range<I>,
143    mut singularities: Vec<I>,
144) -> Vec<std::ops::Range<I>>
145where
146    I: ComplexField + Copy,
147{
148    if singularities.is_empty() {
149        return vec![range];
150    }
151
152    singularities.sort_by(|a, b| a.real().partial_cmp(&b.real()).unwrap());
153
154    let mut points = vec![range.start];
155    for singularity in singularities {
156        points.push(singularity);
157    }
158    points.push(range.end);
159    let epsilon = I::from_f64(1e-8).unwrap();
160
161    let stop = points.len() - 2;
162    let mut new_range = vec![];
163    for (idx, window) in points.windows(2).enumerate() {
164        let left = if idx == 0 {
165            window[0]
166        } else {
167            window[0] + epsilon
168        };
169        let right = if idx == stop {
170            window[1]
171        } else {
172            window[1] - epsilon
173        };
174        new_range.push(left..right);
175    }
176    new_range
177}
178
179#[cfg(test)]
180mod test {
181    use super::{Contour, Direction};
182    use num_complex::Complex;
183    use std::ops::Range;
184
185    #[test]
186    fn simple_pole_contour_integral_evaluates_successfully() {
187        let x_range = Range {
188            start: -0.5,
189            end: 0.5,
190        };
191        let y_range = Range {
192            start: -0.5,
193            end: 0.5,
194        };
195
196        let _contour: Contour<Complex<f64>> =
197            Contour::generate_rectangular(&x_range, &y_range, Direction::Clockwise);
198    }
199}