Skip to main content

ginger/
autocorr.rs

1//! Auto-correlation polynomial root-finding (palindromic polynomials).
2//!
3//! Mirrors `autocorr.hpp` / `autocorr.cpp` in ginger-cpp: the palindromic
4//! variant of Bairstow's method where roots appear in reciprocal pairs.
5
6use crate::execution_policy::{atomic_decoupled_run, jacobi_mt_run, sequential_run, Step};
7use crate::rootfinding::{delta, horner, roots_from_quadratic, suppress_old, Options, Vec2};
8use crate::scaling::scaled_coeffs;
9use crate::seqlock::AtomicVec2;
10use num_complex::Complex;
11
12/// One Bairstow Newton correction respecting palindromic (auto-correlation)
13/// symmetry: each neighbor factor contributes both `vrj` and its reciprocal
14/// image `(-vrj.x, 1) / vrj.y`; the factor itself also suppresses its own
15/// reciprocal image. Shared by the sequential, Jacobi-MT and atomic execution
16/// policies.
17///
18/// This mirrors `ginger::detail::autocorr_bairstow_step` in ginger-cpp.
19pub struct AutocorrBairstowStep<'a> {
20    /// Polynomial coefficients (highest degree first).
21    pub coeffs: &'a [f64],
22    /// Degree of the polynomial.
23    pub degree: usize,
24    /// Convergence options.
25    pub options: &'a Options,
26}
27
28impl Step<Vec2> for AutocorrBairstowStep<'_> {
29    type Cell = AtomicVec2;
30
31    fn run<G, N>(&self, idx: usize, get: G, neighbors: N) -> (f64, Option<Vec2>)
32    where
33        G: Fn(usize) -> Vec2,
34        N: Iterator<Item = usize>,
35    {
36        let vri = get(idx);
37        let mut coeffs1 = self.coeffs.to_owned(); // horner corrupts the array
38        let mut v_big_a = horner(&mut coeffs1, self.degree, &vri);
39        let tol_i = v_big_a.norm_inf();
40        if tol_i < self.options.tol_ind {
41            return (0.0, None);
42        }
43        let mut v_big_a1 = horner(&mut coeffs1, self.degree - 2, &vri);
44        for j in neighbors {
45            let vrj = get(j);
46            suppress_old(&mut v_big_a, &mut v_big_a1, &vri, &vrj);
47            let vrjn = Vec2::new(-vrj.x_, 1.0) / vrj.y_;
48            suppress_old(&mut v_big_a, &mut v_big_a1, &vri, &vrjn);
49        }
50        let vrin = Vec2::new(-vri.x_, 1.0) / vri.y_;
51        suppress_old(&mut v_big_a, &mut v_big_a1, &vri, &vrin);
52        let dt = delta(&v_big_a, &vri, &v_big_a1); // Gauss-Seidel fashion
53        (tol_i, Some(vri - dt))
54    }
55}
56
57/// The `initial_autocorr` function calculates the initial guesses for Bairstow's method for finding
58/// roots of a polynomial, specifically for the auto-correlation function.
59///
60/// $$ R = \sqrt\[n\]{|a_n|},\qquad R \leftarrow \max(R, 1/R),\qquad m = n/2 $$
61/// $$ \theta_k = \frac{k\pi}{m},\qquad (r_k, q_k) = (2R\cos\theta_k,\; -R^2) $$
62///
63/// Arguments:
64///
65/// * `coeffs`: The `coeffs` parameter is a slice of `f64` values representing the coefficients of a
66///   polynomial. The coefficients are ordered from highest degree to lowest degree.
67///
68/// Returns:
69///
70/// The function `initial_autocorr` returns a vector of `Vec2` structs.
71///
72/// # Examples:
73///
74/// ```
75/// use ginger::autocorr::initial_autocorr;
76/// use ginger::vector2::Vector2;
77///
78/// let coeffs = vec![10.0, 34.0, 75.0, 94.0, 150.0, 94.0, 75.0, 34.0, 10.0];
79/// let vr0s = initial_autocorr(&coeffs);
80/// ```
81pub fn initial_autocorr(coeffs: &[f64]) -> Vec<Vec2> {
82    let degree = coeffs.len() - 1;
83    let radius = coeffs[degree].abs().powf(1.0 / (degree as f64));
84    let degree = degree / 2;
85    let m = radius * radius;
86    let num_points = degree / 2;
87    (0..num_points)
88        .map(|i| Vec2::new(2.0 * radius * crate::tables::cos_pi_vdc2(i), -m))
89        .collect()
90}
91
92/// The `pbairstow_autocorr` function implements the simultaneous Bairstow's method for finding roots of
93/// a polynomial, specifically for the auto-correlation function.
94///
95/// Arguments:
96///
97/// * `coeffs`: The `coeffs` parameter is a slice of `f64` values representing the coefficients of a
98///   polynomial. These coefficients are used to calculate the auto-correlation function.
99/// * `vrs`: `vrs` is a vector of complex numbers representing the initial guesses for the roots of the
100///   polynomial. Each element of `vrs` is a `Vec2` struct, which contains two fields: `x_` and `y_`.
101///   These fields represent the real and imaginary parts of the
102/// * `options`: The `Options` struct is used to specify the parameters for the Bairstow's method
103///   algorithm. It has the following fields:
104///
105/// # Examples:
106///
107/// ```
108/// use ginger::autocorr::{initial_autocorr, pbairstow_autocorr};
109/// use ginger::rootfinding::Options;
110///
111/// let coeffs = vec![10.0, 34.0, 75.0, 94.0, 150.0, 94.0, 75.0, 34.0, 10.0];
112/// let mut vrs = initial_autocorr(&coeffs);
113/// let (niter, found) = pbairstow_autocorr(&coeffs, &mut vrs, &Options::default());
114///
115/// assert!(niter > 0);
116/// assert!(found);
117/// ```
118pub fn pbairstow_autocorr(coeffs: &[f64], vrs: &mut [Vec2], options: &Options) -> (usize, bool) {
119    let scaled = scaled_coeffs(coeffs);
120    let step = AutocorrBairstowStep {
121        coeffs: &scaled,
122        degree: scaled.len() - 1,
123        options,
124    };
125    sequential_run(vrs, options, &step)
126}
127
128/// The `pbairstow_autocorr_mt` function is a multi-threaded implementation of Bairstow's method for
129/// finding roots of a polynomial, specifically for auto-correlation functions.
130///
131/// Arguments:
132///
133/// * `coeffs`: The `coeffs` parameter is a slice of `f64` values representing the coefficients of a
134///   polynomial. These coefficients are used as input for the Bairstow's method algorithm.
135/// * `vrs`: `vrs` is a vector of complex numbers representing the initial guesses for the roots of the
136///   polynomial. Each element of `vrs` is a `Vec2` struct, which contains the real and imaginary parts of
137///   the complex number.
138/// * `options`: The `options` parameter is an instance of the `Options` struct, which contains the
139///   following fields:
140///
141/// # Examples:
142///
143/// ```
144/// use ginger::autocorr::{initial_autocorr, pbairstow_autocorr_mt};
145/// use ginger::rootfinding::Options;
146///
147/// let coeffs = vec![10.0, 34.0, 75.0, 94.0, 150.0, 94.0, 75.0, 34.0, 10.0];
148/// let mut vrs = initial_autocorr(&coeffs);
149/// let (niter, found) = pbairstow_autocorr_mt(&coeffs, &mut vrs, &Options::default());
150///
151/// assert!(niter > 0);
152/// assert!(found);
153/// ```
154pub fn pbairstow_autocorr_mt(coeffs: &[f64], vrs: &mut [Vec2], options: &Options) -> (usize, bool) {
155    let scaled = scaled_coeffs(coeffs);
156    let step = AutocorrBairstowStep {
157        coeffs: &scaled,
158        degree: scaled.len() - 1,
159        options,
160    };
161    jacobi_mt_run(vrs, options, &step)
162}
163
164/// Atomic multi-threading Bairstow's method (auto-correlation, decoupled)
165///
166/// The `pbairstow_autocorr_atomic` function is a multi-threaded implementation of Bairstow's
167/// method for finding roots of a polynomial with auto-correlation (palindromic) symmetry using a
168/// single atomic working buffer.
169///
170/// Unlike `pbairstow_autocorr_mt` (Jacobi snapshot + per-iteration barrier), the atomic buffer is
171/// built once: each thread owns a chunk of factor slots (single-writer, multi-reader via a
172/// seqlock) and runs its own iteration loop independently. There is no per-iteration
173/// synchronization — a thread exits as soon as its own chunk converges or the maximum number of
174/// iterations is exceeded. The iteration count is therefore NON-DETERMINISTIC (the returned count
175/// is the maximum across threads), and `found` is true only if every chunk converged.
176///
177/// Arguments:
178///
179/// * `coeffs`: The `coeffs` parameter is a slice of `f64` values representing the coefficients of a
180///   polynomial. These coefficients are used as input for the Bairstow's method algorithm.
181/// * `vrs`: `vrs` is a vector of complex numbers representing the initial guesses for the roots of
182///   the polynomial. Each element of `vrs` is a `Vec2` struct, which contains the real and
183///   imaginary parts of the complex number.
184/// * `options`: The `options` parameter is an instance of the `Options` struct, which contains the
185///   following fields:
186///
187/// # Examples:
188///
189/// ```
190/// use ginger::autocorr::{initial_autocorr, pbairstow_autocorr_atomic};
191/// use ginger::rootfinding::Options;
192///
193/// let coeffs = vec![10.0, 34.0, 75.0, 94.0, 150.0, 94.0, 75.0, 34.0, 10.0];
194/// let mut vrs = initial_autocorr(&coeffs);
195/// let (niter, found) = pbairstow_autocorr_atomic(&coeffs, &mut vrs, &Options::default());
196///
197/// assert!(niter > 0);
198/// assert!(found);
199/// ```
200pub fn pbairstow_autocorr_atomic(
201    coeffs: &[f64],
202    vrs: &mut [Vec2],
203    options: &Options,
204) -> (usize, bool) {
205    let scaled = scaled_coeffs(coeffs);
206    let step = AutocorrBairstowStep {
207        coeffs: &scaled,
208        degree: scaled.len() - 1,
209        options,
210    };
211    atomic_decoupled_run(vrs, options, &step)
212}
213
214/// The `extract_autocorr` function extracts quadratic factors from a polynomial with auto-correlation
215/// property.
216///
217/// Given a quadratic $$ x^2 - r x - q $$, computes its roots and replaces
218/// any root $$ |z| > 1 $$ with its reciprocal $$ 1/z $$. The normalized
219/// factor is recovered from the adjusted roots via Vieta:
220///
221/// $$ r' = z_1' + z_2', \qquad q' = -z_1' z_2' $$
222///
223/// where $$ z_k' = z_k $$ if $$ |z_k| \le 1 $$, else $$ z_k' = 1/z_k $$.
224///
225/// Arguments:
226///
227/// * `vr`: A vector containing two values, representing the coefficients of a quadratic function. The
228///   first value represents the coefficient of x^2, and the second value represents the coefficient of x.
229///
230/// Returns:
231///
232/// The function `extract_autocorr` returns a `Vec2` struct, which contains two elements `x_` and `y_`.
233///
234/// # Examples:
235///
236/// ```
237/// use ginger::autocorr::extract_autocorr;
238/// use ginger::vector2::Vector2;
239/// use approx_eq::assert_approx_eq;
240///
241/// let vr = extract_autocorr(Vector2::new(1.0, -4.0));
242///
243/// assert_approx_eq!(vr.x_, 0.25);
244/// assert_approx_eq!(vr.y_, -0.25);
245/// ```
246pub fn extract_autocorr(vr: Vec2) -> Vec2 {
247    let Vec2 { x_: r, y_: q } = vr;
248    let hr = r / 2.0;
249    let d = hr * hr + q;
250    if d < 0.0 {
251        // complex conjugate root
252        if q < -1.0 {
253            return Vec2::new(-r, 1.0) / q;
254        }
255    }
256    // two real roots
257    let mut a1 = hr + (if hr >= 0.0 { d.sqrt() } else { -d.sqrt() });
258    let mut a2 = -q / a1;
259
260    if a1.abs() > 1.0 {
261        if a2.abs() > 1.0 {
262            a2 = 1.0 / a2;
263        }
264        a1 = 1.0 / a1;
265        return Vec2::new(a1 + a2, -a1 * a2);
266    }
267    if a2.abs() > 1.0 {
268        a2 = 1.0 / a2;
269        return Vec2::new(a1 + a2, -a1 * a2);
270    }
271    // else no need to change
272    vr
273}
274
275/// Reconstruct a monic polynomial from its autocorrelation quadratic factors
276///
277/// Auto-correlation (palindromic) polynomials have roots in reciprocal pairs.
278/// Each quadratic factor $x^2 - r x - q$ found by `pbairstow_autocorr` carries 2 roots.
279/// This function adds the reciprocal of each root, then reconstructs the full
280/// monic polynomial with Leja ordering for numerical accuracy.
281///
282/// $$ P(x) = \prod_{i=1}^{m} (x^2 - r_i x - q_i)(x^{-2} - r_i x^{-1} - q_i) $$
283///
284/// Arguments:
285///
286/// * `vrs` - Quadratic factors from pbairstow_autocorr
287///
288/// Returns:
289///
290/// Monic polynomial coefficients (highest degree first)
291pub fn poly_from_autocorr_factors(vrs: &[Vec2]) -> Vec<f64> {
292    if vrs.is_empty() {
293        return vec![1.0];
294    }
295    // Each factor x^2 - r*x - q contributes 2 roots. For palindromic/autocorrelation
296    // polynomials, the reciprocal of each root is also a root. Collect all roots
297    // and their reciprocals, then reconstruct with Leja ordering.
298    let mut all_roots: Vec<Complex<f64>> = Vec::with_capacity(4 * vrs.len());
299    for vr in vrs {
300        let (r1, r2) = roots_from_quadratic(vr);
301        all_roots.push(r1);
302        all_roots.push(r2);
303        all_roots.push(1.0 / r1);
304        all_roots.push(1.0 / r2);
305    }
306    crate::aberth::poly_from_roots(&all_roots)
307}
308
309#[cfg(test)]
310mod tests {
311    use super::*;
312    use approx_eq::assert_approx_eq;
313
314    #[test]
315    fn test_initial_autocorr() {
316        let coeffs = vec![10.0, 34.0, 75.0, 94.0, 150.0, 94.0, 75.0, 34.0, 10.0];
317        let guesses = initial_autocorr(&coeffs);
318
319        assert_eq!(guesses.len(), 2);
320        // Verify the first guess is reasonable
321        assert!(guesses[0].x_.abs() > 0.0);
322        assert!(guesses[0].y_.abs() > 0.0);
323    }
324
325    #[test]
326    fn test_pbairstow_autocorr() {
327        let coeffs = vec![10.0, 34.0, 75.0, 94.0, 150.0, 94.0, 75.0, 34.0, 10.0];
328        let mut vrs = initial_autocorr(&coeffs);
329        let options = Options::default();
330
331        let (niter, found) = pbairstow_autocorr(&coeffs, &mut vrs, &options);
332
333        assert!(niter > 0);
334        assert!(found);
335        // Verify at least one root is close to actual root
336        let mut has_root = false;
337        for vr in vrs {
338            let val = horner(&mut coeffs.clone(), coeffs.len() - 1, &vr);
339            if val.norm_inf() < options.tolerance {
340                has_root = true;
341                break;
342            }
343        }
344        assert!(has_root);
345    }
346
347    #[test]
348    fn test_pbairstow_autocorr_atomic() {
349        let coeffs = vec![10.0, 34.0, 75.0, 94.0, 150.0, 94.0, 75.0, 34.0, 10.0];
350        let mut vrs = initial_autocorr(&coeffs);
351        let (niter, found) = pbairstow_autocorr_atomic(&coeffs, &mut vrs, &Options::default());
352        assert!(niter > 0);
353        assert!(found);
354    }
355
356    const FIR_COEFFS: [f64; 49] = [
357        -0.00196191,
358        -0.00094597,
359        -0.00023823,
360        0.00134667,
361        0.00380494,
362        0.00681596,
363        0.0097864,
364        0.01186197,
365        0.0121238,
366        0.00985211,
367        0.00474894,
368        -0.00281751,
369        -0.01173923,
370        -0.0201885,
371        -0.02590168,
372        -0.02658216,
373        -0.02035729,
374        -0.00628271,
375        0.01534627,
376        0.04279982,
377        0.0732094,
378        0.10275561,
379        0.12753013,
380        0.14399228,
381        0.15265722,
382        0.14399228,
383        0.12753013,
384        0.10275561,
385        0.0732094,
386        0.04279982,
387        0.01534627,
388        -0.00628271,
389        -0.02035729,
390        -0.02658216,
391        -0.02590168,
392        -0.0201885,
393        -0.01173923,
394        -0.00281751,
395        0.00474894,
396        0.00985211,
397        0.0121238,
398        0.01186197,
399        0.0097864,
400        0.00681596,
401        0.00380494,
402        0.00134667,
403        -0.00023823,
404        -0.00094597,
405        -0.00196191,
406    ];
407
408    #[test]
409    fn test_pbairstow_autocorr_atomic_fir() {
410        // Decoupled threads under load (libtest runs tests in parallel) can need
411        // many iterations to converge; give a generous iteration budget.
412        let options = Options {
413            max_iters: 20000,
414            tolerance: 1e-2,
415            ..Options::default()
416        };
417        let mut vrs = initial_autocorr(&FIR_COEFFS);
418        let (_, found) = pbairstow_autocorr_atomic(&FIR_COEFFS, &mut vrs, &options);
419        assert!(found);
420    }
421
422    #[test]
423    fn test_pbairstow_autocorr_atomic_reconstruction() {
424        let coeffs = vec![10.0, 34.0, 75.0, 94.0, 150.0, 94.0, 75.0, 34.0, 10.0];
425        let mut vrs = initial_autocorr(&coeffs);
426        let (_, found) = pbairstow_autocorr_atomic(&coeffs, &mut vrs, &Options::default());
427        assert!(found);
428        let monic = poly_from_autocorr_factors(&vrs);
429        let scale = coeffs[0];
430        for (i, c) in coeffs.iter().enumerate() {
431            assert!(
432                (monic[i] * scale - c).abs() < 1e-8,
433                "coefficient {i} mismatch: {} vs {}",
434                monic[i] * scale,
435                c
436            );
437        }
438    }
439
440    #[test]
441    fn test_extract_autocorr() {
442        let vr = Vec2::new(1.0, -4.0);
443        let result = extract_autocorr(vr);
444
445        assert_approx_eq!(result.x_, 0.25);
446        assert_approx_eq!(result.y_, -0.25);
447    }
448
449    #[test]
450    fn test_poly_from_autocorr_factors() {
451        let vrs = vec![Vec2::new(3.0, -2.0)];
452        let coeffs = poly_from_autocorr_factors(&vrs);
453        // With reciprocals: roots are 2, 1, 0.5, 1.0
454        // polynomial = (x-2)(x-1)(x-0.5)(x-1) = ...
455        assert_eq!(coeffs.len(), 5);
456        assert!((coeffs[0] - 1.0).abs() < 1e-12);
457    }
458}