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}