Skip to main content

ginger/
rootfinding.rs

1use super::horner::horner_eval_f;
2use super::{Matrix2, Vector2};
3use num_complex::Complex;
4
5type Vec2 = Vector2<f64>;
6type Mat2 = Matrix2<f64>;
7
8/// The below code defines a struct named Options with three fields: max_iters, tolerance, and tol_ind.
9///
10/// Properties:
11///
12/// * `max_iters`: The `max_iters` property represents the maximum number of iterations allowed for a
13///   certain algorithm or process. It is of type `usize`, which means it can only hold non-negative
14///   integer values.
15/// * `tolerance`: The `tolerance` property is a floating-point number that represents the tolerance for convergence
16///   in an algorithm. It is used to determine when the algorithm has reached a satisfactory solution.
17/// * `tol_ind`: The `tol_ind` property in the `Options` struct represents the tolerance for individual
18///   values. It is a floating-point number (`f64`) that determines the acceptable difference between the
19///   expected value and the actual value for each element in a calculation or comparison.
20#[derive(Debug)]
21pub struct Options {
22    pub max_iters: usize,
23    pub tolerance: f64,
24    pub tol_ind: f64,
25}
26
27/// The below code is implementing the `Default` trait for the `Options` struct in Rust. The `Default`
28/// trait provides a default value for a type, which can be used when creating an instance of the type
29/// without specifying any values. In this case, the `default` function is defined to return an instance
30/// of the `Options` struct with default values for the `max_iters`, `tolerance`, and `tol_ind` fields.
31impl Default for Options {
32    fn default() -> Self {
33        Options {
34            max_iters: 2000,
35            tolerance: 1e-12,
36            tol_ind: 1e-15,
37        }
38    }
39}
40
41/// The function `make_adjoint` calculates the adjoint matrix between two vectors.
42///
43/// $$ \text{adj}\!\begin{bmatrix} r & q \\ p & s \end{bmatrix}
44///    = \begin{bmatrix} s & -p \\ -pq & pr+s \end{bmatrix} $$
45///
46/// Arguments:
47///
48/// * `vr`: A vector representing the direction of the reference frame's x-axis.
49/// * `vp`: The parameter `vp` represents a vector `vp = (p, s)`, where `p` and `s` are the components
50///   of the vector.
51///
52/// Returns:
53///
54/// The function `make_adjoint` returns a `Mat2` object.
55/// * `vp`: Another vector representing the row of a 2x2 matrix
56///
57/// Returns:
58///
59/// A 2x2 matrix representing the adjoint
60#[inline]
61pub fn make_adjoint(vr: &Vec2, vp: &Vec2) -> Mat2 {
62    let (r, q) = (vr.x_, vr.y_);
63    let (p, s) = (vp.x_, vp.y_);
64    Mat2::new(
65        Vector2::<f64>::new(s, -p),
66        Vector2::<f64>::new(-p * q, p * r + s),
67    )
68}
69
70/// The function `make_inverse` calculates the inverse of a 2x2 matrix.
71///
72/// $$ \mathbf{M}^{-1} = \frac{\text{adj}(\mathbf{M})}{\det(\mathbf{M})} $$
73///
74/// Arguments:
75///
76/// * `vr`: A vector representing the row of a 2x2 matrix. The components of the vector are vr.x_ and vr.y_.
77/// * `vp`: The parameter `vp` represents a 2D vector with components `x` and `y`.
78///
79/// Returns:
80///
81/// The function `make_inverse` returns a `Mat2` object.
82/// * `vp`: Another vector representing the row of a 2x2 matrix
83///
84/// Returns:
85///
86/// A 2x2 matrix representing the inverse
87#[inline]
88pub fn make_inverse(vr: &Vec2, vp: &Vec2) -> Mat2 {
89    let (r, q) = (vr.x_, vr.y_);
90    let (p, s) = (vp.x_, vp.y_);
91    let m_adjoint = Mat2::new(
92        Vector2::<f64>::new(s, -p),
93        Vector2::<f64>::new(-p * q, p * r + s),
94    );
95    m_adjoint / m_adjoint.det()
96}
97
98/// The `delta` function calculates the adjustment vector for Bairstow's method.
99///
100/// Solves the 2x2 linear system for the optimal adjustment to current quadratic
101/// factor estimates $$ (r, q) $$:
102///
103/// $$ \begin{bmatrix} r p + s & p \\ q p & s \end{bmatrix}
104///    \begin{bmatrix} \Delta r \\ \Delta q \end{bmatrix}
105///    = \begin{bmatrix} A \\ B \end{bmatrix} $$
106///
107/// where $$ (p, s) = (r_i - r_j,\; q_i - q_j) $$ is the difference between
108/// two factor estimates, and $$ (A, B) $$ is the remainder from polynomial division.
109///
110/// Arguments:
111///
112/// * `vA`: A vector representing the coefficients of a polynomial equation.
113/// * `vr`: The parameter `vr` represents the vector `[-2.0, 0.0]`.
114/// * `vp`: The parameter `vp` represents the vector vr - vrj
115///
116/// # Examples:
117///
118/// ```
119/// use ginger::rootfinding::delta;
120/// use ginger::vector2::Vector2;
121///
122/// let mut vA1 = Vector2::new(1.0, 2.0);
123/// let vri = Vector2::new(-2.0, 0.0);
124/// let vrj = Vector2::new(4.0, 5.0);
125/// let vd = delta(&vA1, &vri, &vrj);
126/// assert_eq!(vd, Vector2::new(0.2, 0.4));
127/// ```
128#[inline]
129pub fn delta(vA: &Vec2, vr: &Vec2, vp: &Vec2) -> Vec2 {
130    let mp = make_adjoint(vr, vp); // 2 mul's
131    mp.mdot(vA) / mp.det() // 6 mul's + 2 div's
132}
133
134/// delta 1 for ri - rj
135///
136/// # Examples:
137///
138/// ```
139/// use ginger::rootfinding::delta1;
140/// use ginger::vector2::Vector2;
141///
142/// let mut vA1 = Vector2::new(1.0, 2.0);
143/// let vri = Vector2::new(-2.0, -0.0);
144/// let vrj = Vector2::new(4.0, -5.0);
145/// let vd = delta1(&vA1, &vri, &vrj);
146/// assert_eq!(vd, Vector2::new(0.2, 0.4));
147/// ```
148#[inline]
149pub fn delta1(vA: &Vec2, vr: &Vec2, vp: &Vec2) -> Vec2 {
150    let (r, q) = (vr.x_, vr.y_);
151    let (p, s) = (vp.x_, vp.y_);
152    let mp = Matrix2::new(Vec2::new(-s, -p), Vec2::new(p * q, p * r - s));
153    mp.mdot(vA) / mp.det() // 6 mul's + 2 div's
154}
155
156/// The `suppress_old` function performs zero suppression on a set of vectors.
157///
158/// Arguments:
159///
160/// * `vA`: A mutable reference to a Vector2 object representing the coefficients of a polynomial. The
161///   coefficients are stored in the x_ and y_ fields of the Vector2 object.
162/// * `vA1`: vA1 is a mutable reference to a Vector2 object.
163/// * `vri`: The parameter `vri` represents a vector with components `r` and `i`. It is used in the
164///   `suppress_old` function to perform calculations.
165/// * `vrj`: The parameter `vrj` represents a vector with components `x` and `y`.
166///
167/// # Examples:
168///
169/// ```
170/// use ginger::rootfinding::delta;
171/// use ginger::rootfinding::suppress_old;
172/// use ginger::vector2::Vector2;
173/// use approx_eq::assert_approx_eq;
174///
175/// let mut vA = Vector2::new(3.0, 3.0);
176/// let mut vA1 = Vector2::new(1.0, 2.0);
177/// let vri = Vector2::new(-2.0, 0.0);
178/// let vrj = Vector2::new(4.0, 5.0);
179///
180/// suppress_old(&mut vA, &mut vA1, &vri, &vrj);
181/// let dr = delta(&vA, &vri, &vA1);
182/// assert_approx_eq!(dr.x_, -16.780821917808325);
183/// assert_approx_eq!(dr.y_, 1.4383561643835612);
184#[inline]
185pub fn suppress_old(vA: &mut Vec2, vA1: &mut Vec2, vri: &Vec2, vrj: &Vec2) {
186    let (A, B) = (vA.x_, vA.y_);
187    let (A1, B1) = (vA1.x_, vA1.y_);
188    let vp = vri - vrj;
189    let (r, q) = (vri.x_, vri.y_);
190    let (p, s) = (vp.x_, vp.y_);
191    let f = (r * p) + s;
192    let qp = q * p;
193    let e = (f * s) - (qp * p);
194    let a = ((A * s) - (B * p)) / e;
195    let b = ((B * f) - (A * qp)) / e;
196    let c = A1 - a;
197    let d = (B1 - b) - (a * p);
198    vA.x_ = a;
199    vA.y_ = b;
200    vA1.x_ = ((c * s) - (d * p)) / e;
201    vA1.y_ = ((d * f) - (c * qp)) / e;
202}
203
204/// The `suppress` function in Rust performs zero suppression on a set of vectors.
205///
206/// Arguments:
207///
208/// * `vA`: A vector representing the coefficients of a polynomial function.
209/// * `vA1`: The parameter `vA1` is a `Vector2` object representing a vector with two components. It is
210///   used as an input parameter in the `suppress` function.
211/// * `vri`: The parameter `vri` represents the vector `ri`, and `vrj` represents the vector `rj`. These
212///   vectors are used in the calculation of the suppression step in the Bairstow's method for root
213///   finding.
214/// * `vrj`: The parameter `vrj` represents a vector with coordinates (4.0, 5.0).
215///   Zero suppression
216///
217/// # Examples:
218///
219/// ```
220/// use ginger::rootfinding::delta;
221/// use ginger::rootfinding::suppress;
222/// use ginger::vector2::Vector2;
223/// use approx_eq::assert_approx_eq;
224///
225/// let mut vA = Vector2::new(3.0, 3.0);
226/// let mut vA1 = Vector2::new(1.0, 2.0);
227/// let vri = Vector2::new(-2.0, 0.0);
228/// let vrj = Vector2::new(4.0, 5.0);
229///
230/// (vA, vA1) = suppress(&mut vA, &mut vA1, &vri, &vrj);
231/// let dr = delta(&vA, &vri, &vA1);
232/// assert_approx_eq!(dr.x_, -16.780821917808325);
233/// assert_approx_eq!(dr.y_, 1.4383561643835612);
234#[inline]
235pub fn suppress(vA: &Vec2, vA1: &Vec2, vri: &Vec2, vrj: &Vec2) -> (Vec2, Vec2) {
236    let vp = vri - vrj;
237    let m_inverse = make_inverse(vri, &vp);
238    let va = m_inverse.mdot(vA);
239    let mut vc = vA1 - va;
240    vc.y_ -= va.x_ * vp.x_;
241    let va1 = m_inverse.mdot(&vc);
242    (va, va1)
243}
244
245/// The `horner` function implements synthetic division by a quadratic factor $$ x^2 - r x - q $$.
246///
247/// Given polynomial $$ P(x) = \sum_{k=0}^{n} a_k x^{n-k} $$, the recurrence for the quotient
248/// coefficients $$ b_k $$ is:
249///
250/// $$ b_0 = a_0,\quad b_1 = a_1 + r b_0,\quad b_k = a_k + r b_{k-1} + q b_{k-2} $$
251///
252/// with remainder $$ A = b_{n-1},\; B = b_n + q b_{n-1} $$.
253///
254/// Arguments:
255///
256/// * `coeffs`: A mutable slice of f64 values representing the coefficients of the polynomial. The
257///   coefficients are in descending order of degree.
258/// * `degree`: The `degree` parameter represents the degree of the polynomial. It is used to determine
259///   the number of coefficients in the `coeffs` array.
260/// * `vr`: The parameter `vr` is a `Vec2` struct that contains two values, `x_` and `y_`. In the
261///   example, `vr` is initialized with the values `-1.0` and `-2.0`.
262///
263/// Returns:
264///
265/// The function `horner` returns a `Vec2` struct, which contains two `f64` values representing the
266/// remainder $$ (A, B) $$ of the synthetic division.
267///
268/// # Examples:
269///
270/// ```
271/// use ginger::rootfinding::horner;
272/// use ginger::vector2::Vector2;
273/// use approx_eq::assert_approx_eq;
274///
275/// let mut coeffs = vec![10.0, 34.0, 75.0, 94.0, 150.0, 94.0, 75.0, 34.0, 10.0];
276/// let px = horner(&mut coeffs, 8, &Vector2::new(-1.0, -2.0));
277///
278/// assert_approx_eq!(px.x_, 114.0);
279/// assert_approx_eq!(px.y_, 134.0);
280/// assert_approx_eq!(coeffs[3], 15.0);
281/// ```
282pub fn horner(coeffs: &mut [f64], degree: usize, vr: &Vec2) -> Vec2 {
283    let Vec2 { x_: r, y_: q } = vr;
284    for idx in 0..(degree - 1) {
285        coeffs[idx + 1] += coeffs[idx] * r;
286        coeffs[idx + 2] += coeffs[idx] * q;
287    }
288    Vector2::<f64>::new(coeffs[degree - 1], coeffs[degree])
289}
290
291/// The `initial_guess` function generates initial quadratic factor estimates for Bairstow's method.
292///
293/// Estimates are placed around a circle centered at $$ c $$ with radius $$ R $$:
294///
295/// $$ c = -\frac{a_1}{n a_0}, \qquad R = \sqrt\[n\]{|P(c)|} $$
296///
297/// where the angular positions come from a van der Corput low-discrepancy sequence.
298///
299/// Arguments:
300///
301/// * `coeffs`: A vector of coefficients representing a polynomial.
302///
303/// Returns:
304///
305/// The function `initial_guess` returns a vector of `Vector2` structs, which represent the initial
306/// guesses for the roots of a polynomial equation.
307///
308/// # Examples:
309///
310/// ```
311/// use ginger::rootfinding::initial_guess;
312/// use ginger::vector2::Vector2;
313///
314/// let coeffs = vec![10.0, 34.0, 75.0, 94.0, 150.0, 94.0, 75.0, 34.0, 10.0];
315/// let vr0s = initial_guess(&coeffs);
316/// ```
317pub fn initial_guess(coeffs: &[f64]) -> Vec<Vec2> {
318    let mut degree = coeffs.len() - 1;
319    let center = -coeffs[1] / (coeffs[0] * degree as f64);
320    let centroid = horner_eval_f(coeffs, center); // ???
321    let radius = centroid.abs().powf(1.0 / (degree as f64));
322    degree /= 2;
323    degree *= 2; // make even
324    let m = center * center + radius * radius;
325    let num_points = degree / 2;
326    (0..num_points)
327        .map(|i| {
328            let temp = radius * crate::tables::cos_pi_vdc2(i);
329            let r0 = 2.0 * (center + temp);
330            let t0 = m + 2.0 * center * temp;
331            Vector2::<f64>::new(r0, -t0)
332        })
333        .collect()
334}
335
336/// Parallel Bairstow's method (even degree only)
337///
338/// The `pbairstow_even` function implements the parallel Bairstow's method for finding roots of
339/// even-degree polynomials.
340///
341/// Arguments:
342///
343/// * `coeffs`: The `coeffs` parameter is a slice of `f64` values representing the coefficients of a polynomial.
344///   It is assumed that the polynomial has an even degree.
345/// * `vrs`: A vector of initial guesses for the roots of the polynomial. Each element of the vector is
346///   a complex number representing a root guess.
347/// * `options`: The `options` parameter is an instance of the `Options` struct, which contains the
348///   following fields:
349///
350/// # Examples:
351///
352/// ```
353/// use ginger::rootfinding::{initial_guess, pbairstow_even, Options};
354///
355/// let coeffs = vec![10.0, 34.0, 75.0, 94.0, 150.0, 94.0, 75.0, 34.0, 10.0];
356/// let mut vrs = initial_guess(&coeffs);
357/// let (niter, found) = pbairstow_even(&coeffs, &mut vrs, &Options::default());
358///
359/// assert!(niter > 0);
360/// assert!(found);
361/// ```
362pub fn pbairstow_even(coeffs: &[f64], vrs: &mut [Vec2], options: &Options) -> (usize, bool) {
363    let m_rs = vrs.len();
364    let mut converged = vec![false; m_rs];
365
366    for niter in 1..options.max_iters {
367        let mut tolerance = 0.0;
368        for i in 0..m_rs {
369            if converged[i] {
370                continue;
371            }
372            let mut vri = vrs[i];
373            if let Some(tol_i) = pbairstow_even_job(coeffs, i, &mut vri, &mut converged[i], vrs) {
374                if tolerance < tol_i {
375                    tolerance = tol_i;
376                }
377            }
378            vrs[i] = vri;
379        }
380        if tolerance < options.tolerance {
381            return (niter, true);
382        }
383    }
384    (options.max_iters, false)
385}
386
387/// Multi-threading Bairstow's method (even degree only)
388///
389/// The `pbairstow_even_mt` function implements the multi-threading parallel Bairstow's
390/// method for finding roots of even-degree polynomials.
391///
392/// Arguments:
393///
394/// * `coeffs`: The `coeffs` parameter is a slice of `f64` values representing the coefficients of a polynomial.
395///   It is assumed that the polynomial has an even degree.
396/// * `vrs`: A vector of initial guesses for the roots of the polynomial. Each element of the vector is
397///   a complex number representing a root guess.
398/// * `options`: The `options` parameter is an instance of the `Options` struct, which contains the
399///   following fields:
400///
401/// # Examples:
402///
403/// ```
404/// use ginger::rootfinding::{initial_guess, pbairstow_even_mt, Options};
405///
406/// let coeffs = vec![10.0, 34.0, 75.0, 94.0, 150.0, 94.0, 75.0, 34.0, 10.0];
407/// let mut vrs = initial_guess(&coeffs);
408/// let (niter, found) = pbairstow_even_mt(&coeffs, &mut vrs, &Options::default());
409///
410/// assert!(niter > 0);
411/// assert!(found);
412/// ```
413pub fn pbairstow_even_mt(coeffs: &[f64], vrs: &mut Vec<Vec2>, options: &Options) -> (usize, bool) {
414    use rayon::prelude::*;
415
416    let m_rs = vrs.len();
417    let mut vrsc = vec![Vec2::default(); m_rs];
418    let mut converged = vec![false; m_rs];
419
420    for niter in 1..options.max_iters {
421        let mut tolerance = 0.0;
422        vrsc.copy_from_slice(vrs);
423
424        let tol_i = vrs
425            .par_iter_mut()
426            .zip(converged.par_iter_mut())
427            .enumerate()
428            .filter(|(_, (_, converged))| !**converged)
429            .filter_map(|(i, (vri, converged))| {
430                pbairstow_even_job(coeffs, i, vri, converged, &vrsc)
431            })
432            .reduce(|| tolerance, |x, y| x.max(y));
433        if tolerance < tol_i {
434            tolerance = tol_i;
435        }
436        if tolerance < options.tolerance {
437            return (niter, true);
438        }
439    }
440    (options.max_iters, false)
441}
442
443/// Internal job function for parallel Bairstow's method (even degree)
444///
445/// Performs a single iteration of Bairstow's method for one root approximation,
446/// suppressing the effect of other roots (Gauss-Seidel style).
447///
448/// Arguments:
449///
450/// * `coeffs`: Polynomial coefficients
451/// * `i`: Current root index
452/// * `vri`: Current root approximation (mutable)
453/// * `converged`: Convergence flag for this root
454/// * `vrsc`: Current approximations of all roots
455///
456/// Returns:
457///
458/// Option containing tolerance value if not yet converged
459fn pbairstow_even_job(
460    coeffs: &[f64],
461    i: usize,
462    vri: &mut Vec2,
463    converged: &mut bool,
464    vrsc: &[Vec2],
465) -> Option<f64> {
466    let mut coeffs1 = coeffs.to_owned();
467    let degree = coeffs1.len() - 1; // degree, assume even
468    let mut vA = horner(&mut coeffs1, degree, vri);
469    let tol_i = vA.norm_inf();
470    if tol_i < 1e-15 {
471        *converged = true;
472        return None;
473    }
474    let mut vA1 = horner(&mut coeffs1, degree - 2, vri);
475    for (_, vrj) in vrsc.iter().enumerate().filter(|t| t.0 != i) {
476        suppress_old(&mut vA, &mut vA1, vri, vrj);
477    }
478    let dt = delta(&vA, vri, &vA1); // Gauss-Seidel fashion
479    *vri -= dt;
480    Some(tol_i)
481}
482
483/// The `initial_autocorr` function calculates the initial guesses for Bairstow's method for finding
484/// roots of a polynomial, specifically for the auto-correlation function.
485///
486/// $$ R = \sqrt\[n\]{|a_n|},\qquad R \leftarrow \max(R, 1/R),\qquad m = n/2 $$
487/// $$ \theta_k = \frac{k\pi}{m},\qquad (r_k, q_k) = (2R\cos\theta_k,\; -R^2) $$
488///
489/// Arguments:
490///
491/// * `coeffs`: The `coeffs` parameter is a slice of `f64` values representing the coefficients of a
492///   polynomial. The coefficients are ordered from highest degree to lowest degree.
493///
494/// Returns:
495///
496/// The function `initial_autocorr` returns a vector of `Vec2` structs.
497///
498/// # Examples:
499///
500/// ```
501/// use ginger::rootfinding::initial_autocorr;
502/// use ginger::vector2::Vector2;
503///
504/// let coeffs = vec![10.0, 34.0, 75.0, 94.0, 150.0, 94.0, 75.0, 34.0, 10.0];
505/// let vr0s = initial_autocorr(&coeffs);
506/// ```
507pub fn initial_autocorr(coeffs: &[f64]) -> Vec<Vec2> {
508    let degree = coeffs.len() - 1;
509    let radius = coeffs[degree].abs().powf(1.0 / (degree as f64));
510    let degree = degree / 2;
511    let m = radius * radius;
512    let num_points = degree / 2;
513    (0..num_points)
514        .map(|i| Vector2::<f64>::new(2.0 * radius * crate::tables::cos_pi_vdc2(i), -m))
515        .collect()
516}
517
518/// The `pbairstow_autocorr` function implements the simultaneous Bairstow's method for finding roots of
519/// a polynomial, specifically for the auto-correlation function.
520///
521/// Arguments:
522///
523/// * `coeffs`: The `coeffs` parameter is a slice of `f64` values representing the coefficients of a
524///   polynomial. These coefficients are used to calculate the auto-correlation function.
525/// * `vrs`: `vrs` is a vector of complex numbers representing the initial guesses for the roots of the
526///   polynomial. Each element of `vrs` is a `Vec2` struct, which contains two fields: `x_` and `y_`.
527///   These fields represent the real and imaginary parts of the
528/// * `options`: The `Options` struct is used to specify the parameters for the Bairstow's method
529///   algorithm. It has the following fields:
530///
531/// # Examples:
532///
533/// ```
534/// use ginger::rootfinding::{initial_autocorr, pbairstow_autocorr, Options};
535///
536/// let coeffs = vec![10.0, 34.0, 75.0, 94.0, 150.0, 94.0, 75.0, 34.0, 10.0];
537/// let mut vrs = initial_autocorr(&coeffs);
538/// let (niter, found) = pbairstow_autocorr(&coeffs, &mut vrs, &Options::default());
539///
540/// assert!(niter > 0);
541/// assert!(found);
542/// ```
543pub fn pbairstow_autocorr(coeffs: &[f64], vrs: &mut [Vec2], options: &Options) -> (usize, bool) {
544    let m_rs = vrs.len();
545    let mut converged = vec![false; m_rs];
546
547    for niter in 0..options.max_iters {
548        let mut tolerance = 0.0;
549
550        for i in 0..m_rs {
551            if converged[i] {
552                continue;
553            }
554            let mut vri = vrs[i];
555            let tol_i = pbairstow_autocorr_mt_job(coeffs, i, &mut vri, &mut converged[i], vrs);
556            if let Some(tol_i) = tol_i {
557                if tolerance < tol_i {
558                    tolerance = tol_i;
559                }
560            }
561            vrs[i] = vri;
562        }
563        if tolerance < options.tolerance {
564            return (niter, true);
565        }
566    }
567    (options.max_iters, false)
568}
569
570/// The `pbairstow_autocorr_mt` function is a multi-threaded implementation of Bairstow's method for
571/// finding roots of a polynomial, specifically for auto-correlation functions.
572///
573/// Arguments:
574///
575/// * `coeffs`: The `coeffs` parameter is a slice of `f64` values representing the coefficients of a
576///   polynomial. These coefficients are used as input for the Bairstow's method algorithm.
577/// * `vrs`: `vrs` is a vector of complex numbers representing the initial guesses for the roots of the
578///   polynomial. Each element of `vrs` is a `Vec2` struct, which contains the real and imaginary parts of
579///   the complex number.
580/// * `options`: The `options` parameter is an instance of the `Options` struct, which contains the
581///   following fields:
582///
583/// # Examples:
584///
585/// ```
586/// use ginger::rootfinding::{initial_autocorr, pbairstow_autocorr_mt, Options};
587///
588/// let coeffs = vec![10.0, 34.0, 75.0, 94.0, 150.0, 94.0, 75.0, 34.0, 10.0];
589/// let mut vrs = initial_autocorr(&coeffs);
590/// let (niter, found) = pbairstow_autocorr_mt(&coeffs, &mut vrs, &Options::default());
591///
592/// assert!(niter > 0);
593/// assert!(found);
594/// ```
595pub fn pbairstow_autocorr_mt(
596    coeffs: &[f64],
597    vrs: &mut Vec<Vec2>,
598    options: &Options,
599) -> (usize, bool) {
600    use rayon::prelude::*;
601
602    let m_rs = vrs.len();
603    let mut vrsc = vec![Vec2::default(); m_rs];
604    let mut converged = vec![false; m_rs];
605
606    for niter in 1..options.max_iters {
607        let mut tolerance = 0.0;
608        vrsc.copy_from_slice(vrs);
609
610        let tol_i = vrs
611            .par_iter_mut()
612            .zip(converged.par_iter_mut())
613            .enumerate()
614            .filter(|(_, (_, converged))| !**converged)
615            .filter_map(|(i, (vri, converged))| {
616                pbairstow_autocorr_mt_job(coeffs, i, vri, converged, &vrsc)
617            })
618            .reduce(|| tolerance, |x, y| x.max(y));
619        if tolerance < tol_i {
620            tolerance = tol_i;
621        }
622        if tolerance < options.tolerance {
623            return (niter, true);
624        }
625    }
626    (options.max_iters, false)
627}
628
629/// Internal job function for parallel Bairstow's method (auto-correlation)
630///
631/// Performs a single iteration of Bairstow's method for auto-correlation polynomials,
632/// considering both roots and their reciprocals.
633///
634/// Arguments:
635///
636/// * `coeffs`: Polynomial coefficients
637/// * `i`: Current root index
638/// * `vri`: Current root approximation (mutable)
639/// * `converged`: Convergence flag for this root
640/// * `vrsc`: Current approximations of all roots
641///
642/// Returns:
643///
644/// Option containing tolerance value if not yet converged
645fn pbairstow_autocorr_mt_job(
646    coeffs: &[f64],
647    i: usize,
648    vri: &mut Vec2,
649    converged: &mut bool,
650    vrsc: &[Vec2],
651) -> Option<f64> {
652    let mut coeffs1 = coeffs.to_owned();
653    let degree = coeffs1.len() - 1; // assumed divided by 4
654    let mut vA = horner(&mut coeffs1, degree, vri);
655    let tol_i = vA.norm_inf();
656    if tol_i < 1e-15 {
657        *converged = true;
658        return None;
659    }
660    let mut vA1 = horner(&mut coeffs1, degree - 2, vri);
661    for (_j, vrj) in vrsc.iter().enumerate().filter(|t| t.0 != i) {
662        suppress_old(&mut vA, &mut vA1, vri, vrj);
663        let vrjn = Vector2::<f64>::new(-vrj.x_, 1.0) / vrj.y_;
664        suppress_old(&mut vA, &mut vA1, vri, &vrjn);
665    }
666    let vrin = Vector2::<f64>::new(-vri.x_, 1.0) / vri.y_;
667    suppress_old(&mut vA, &mut vA1, vri, &vrin);
668    let dt = delta(&vA, vri, &vA1); // Gauss-Seidel fashion
669    *vri -= dt;
670    Some(tol_i)
671}
672
673/// The `extract_autocorr` function extracts quadratic factors from a polynomial with auto-correlation
674/// property.
675///
676/// Given a quadratic $$ x^2 - r x - q $$, computes its roots and replaces
677/// any root $$ |z| > 1 $$ with its reciprocal $$ 1/z $$. The normalized
678/// factor is recovered from the adjusted roots via Vieta:
679///
680/// $$ r' = z_1' + z_2', \qquad q' = -z_1' z_2' $$
681///
682/// where $$ z_k' = z_k $$ if $$ |z_k| \le 1 $$, else $$ z_k' = 1/z_k $$.
683///
684/// Arguments:
685///
686/// * `vr`: A vector containing two values, representing the coefficients of a quadratic function. The
687///   first value represents the coefficient of x^2, and the second value represents the coefficient of x.
688///
689/// Returns:
690///
691/// The function `extract_autocorr` returns a `Vec2` struct, which contains two elements `x_` and `y_`.
692///
693/// # Examples:
694///
695/// ```
696/// use ginger::rootfinding::extract_autocorr;
697/// use ginger::vector2::Vector2;
698/// use approx_eq::assert_approx_eq;
699///
700/// let vr = extract_autocorr(Vector2::new(1.0, -4.0));
701///
702/// assert_approx_eq!(vr.x_, 0.25);
703/// assert_approx_eq!(vr.y_, -0.25);
704/// ```
705pub fn extract_autocorr(vr: Vec2) -> Vec2 {
706    let Vec2 { x_: r, y_: q } = vr;
707    let hr = r / 2.0;
708    let d = hr * hr + q;
709    if d < 0.0 {
710        // complex conjugate root
711        if q < -1.0 {
712            return Vector2::<f64>::new(-r, 1.0) / q;
713        }
714    }
715    // two real roots
716    let mut a1 = hr + (if hr >= 0.0 { d.sqrt() } else { -d.sqrt() });
717    let mut a2 = -q / a1;
718
719    if a1.abs() > 1.0 {
720        if a2.abs() > 1.0 {
721            a2 = 1.0 / a2;
722        }
723        a1 = 1.0 / a1;
724        return Vector2::<f64>::new(a1 + a2, -a1 * a2);
725    }
726    if a2.abs() > 1.0 {
727        a2 = 1.0 / a2;
728        return Vector2::<f64>::new(a1 + a2, -a1 * a2);
729    }
730    // else no need to change
731    vr
732}
733
734/// Extract the two roots from a quadratic factor $$ x^2 - r x - q $$
735///
736/// $$ x = \frac{r \pm \sqrt{r^2 + 4q}}{2} $$
737///
738/// Given a quadratic factor represented as Vec2 where x() = r and y() = -q
739/// (i.e., x^2 - r*x - q), return the two roots as complex numbers.
740fn roots_from_quadratic(vr: &Vec2) -> (Complex<f64>, Complex<f64>) {
741    let r = vr.x_;
742    let q = vr.y_;
743    let disc = r * r + 4.0 * q;
744    if disc >= 0.0 {
745        let sqrt_disc = disc.sqrt();
746        (
747            Complex::new((r + sqrt_disc) / 2.0, 0.0),
748            Complex::new((r - sqrt_disc) / 2.0, 0.0),
749        )
750    } else {
751        let sqrt_disc = (-disc).sqrt();
752        (
753            Complex::new(r / 2.0, sqrt_disc / 2.0),
754            Complex::new(r / 2.0, -sqrt_disc / 2.0),
755        )
756    }
757}
758
759/// Reconstruct a monic polynomial from its quadratic factors
760///
761/// $$ P(x) = \prod_{i=1}^{m} (x^2 - r_i x - q_i) $$
762///
763/// Given the quadratic factors found by Bairstow's method (each representing
764/// x^2 - r*x - q), multiply them together to recover the monic polynomial
765/// coefficients. To get the original polynomial, multiply the result by the
766/// original leading coefficient.
767///
768/// Arguments:
769///
770/// * `vrs` - Quadratic factors from pbairstow_even, each as a Vec2 with x() = r, y() = q
771///
772/// Returns:
773///
774/// Monic polynomial coefficients (highest degree first)
775pub fn poly_from_quadratic_factors(vrs: &[Vec2]) -> Vec<f64> {
776    if vrs.is_empty() {
777        return vec![1.0];
778    }
779    // Extract all roots from quadratic factors and reconstruct with Leja ordering
780    let mut all_roots: Vec<Complex<f64>> = Vec::with_capacity(2 * vrs.len());
781    for vr in vrs {
782        let (r1, r2) = roots_from_quadratic(vr);
783        all_roots.push(r1);
784        all_roots.push(r2);
785    }
786    crate::aberth::poly_from_roots(&all_roots)
787}
788
789/// Reconstruct a monic polynomial from its autocorrelation quadratic factors
790///
791/// Auto-correlation (palindromic) polynomials have roots in reciprocal pairs.
792/// Each quadratic factor x^2 - r*x - q found by pbairstow_autocorr carries 2 roots.
793/// This function adds the reciprocal of each root, then reconstructs the full
794/// monic polynomial with Leja ordering for numerical accuracy.
795///
796/// Arguments:
797///
798/// * `vrs` - Quadratic factors from pbairstow_autocorr
799///
800/// Returns:
801///
802/// Monic polynomial coefficients (highest degree first)
803pub fn poly_from_autocorr_factors(vrs: &[Vec2]) -> Vec<f64> {
804    if vrs.is_empty() {
805        return vec![1.0];
806    }
807    // Each factor x^2 - r*x - q contributes 2 roots. For palindromic/autocorrelation
808    // polynomials, the reciprocal of each root is also a root. Collect all roots
809    // and their reciprocals, then reconstruct with Leja ordering.
810    let mut all_roots: Vec<Complex<f64>> = Vec::with_capacity(4 * vrs.len());
811    for vr in vrs {
812        let (r1, r2) = roots_from_quadratic(vr);
813        all_roots.push(r1);
814        all_roots.push(r2);
815        all_roots.push(1.0 / r1);
816        all_roots.push(1.0 / r2);
817    }
818    crate::aberth::poly_from_roots(&all_roots)
819}
820
821#[cfg(test)]
822mod tests {
823    use super::*;
824    use approx_eq::assert_approx_eq;
825
826    #[test]
827    fn test_options_default() {
828        let options = Options::default();
829        assert_eq!(options.max_iters, 2000);
830        assert_eq!(options.tolerance, 1e-12);
831        assert_eq!(options.tol_ind, 1e-15);
832    }
833
834    // #[test]
835    // fn test_make_adjoint() {
836    //     let vr = Vector2::new(1.0, 2.0);
837    //     let vp = Vector2::new(3.0, 4.0);
838    //     let adjoint = make_adjoint(&vr, &vp);
839
840    //     assert_eq!(adjoint.x_.x_, 4.0);
841    //     assert_eq!(adjoint.x_.y_, -3.0);
842    //     assert_eq!(adjoint.y_.x_, -6.0);
843    //     assert_eq!(adjoint.y_.y_, 11.0);
844    // }
845
846    // #[test]
847    // fn test_make_inverse() {
848    //     let vr = Vector2::new(1.0, 2.0);
849    //     let vp = Vector2::new(3.0, 4.0);
850    //     let inverse = make_inverse(&vr, &vp);
851
852    //     // Verify inverse by multiplying with original matrix
853    //     let original = Matrix2::new(vr, Vector2::new(vp.x_, 0.0));
854    //     let product = original * inverse;
855    //     assert_approx_eq!(product.x_.x_, 1.0);
856    //     assert_approx_eq!(product.x_.y_, 0.0);
857    //     assert_approx_eq!(product.y_.x_, 0.0);
858    //     assert_approx_eq!(product.y_.y_, 1.0);
859    // }
860
861    #[test]
862    fn test_delta() {
863        let vA = Vector2::new(1.0, 2.0);
864        let vr = Vector2::new(-2.0, 0.0);
865        let vp = Vector2::new(4.0, 5.0);
866        let delta = delta(&vA, &vr, &vp);
867
868        assert_approx_eq!(delta.x_, 0.2);
869        assert_approx_eq!(delta.y_, 0.4);
870    }
871
872    #[test]
873    fn test_suppress_old() {
874        let mut vA = Vector2::new(3.0, 3.0);
875        let mut vA1 = Vector2::new(1.0, 2.0);
876        let vri = Vector2::new(-2.0, 0.0);
877        let vrj = Vector2::new(4.0, 5.0);
878
879        suppress_old(&mut vA, &mut vA1, &vri, &vrj);
880        let dr = delta(&vA, &vri, &vA1);
881        assert_approx_eq!(dr.x_, -16.780821917808325);
882        assert_approx_eq!(dr.y_, 1.4383561643835612);
883    }
884
885    #[test]
886    fn test_suppress() {
887        let vA = Vector2::new(3.0, 3.0);
888        let vA1 = Vector2::new(1.0, 2.0);
889        let vri = Vector2::new(-2.0, 0.0);
890        let vrj = Vector2::new(4.0, 5.0);
891
892        let (va, va1) = suppress(&vA, &vA1, &vri, &vrj);
893        let dr = delta(&va, &vri, &va1);
894        assert_approx_eq!(dr.x_, -16.780821917808325);
895        assert_approx_eq!(dr.y_, 1.4383561643835612);
896    }
897
898    #[test]
899    fn test_horner_eval() {
900        let coeffs = vec![10.0, 34.0, 75.0, 94.0, 150.0, 94.0, 75.0, 34.0, 10.0];
901        let result = horner_eval_f(&coeffs, 2.0);
902
903        assert_eq!(result, 18250.0);
904    }
905
906    #[test]
907    fn test_horner() {
908        let mut coeffs = vec![10.0, 34.0, 75.0, 94.0, 150.0, 94.0, 75.0, 34.0, 10.0];
909        let vr = Vector2::new(-1.0, -2.0);
910        let result = horner(&mut coeffs, 8, &vr);
911
912        assert_approx_eq!(result.x_, 114.0);
913        assert_approx_eq!(result.y_, 134.0);
914        assert_eq!(coeffs[3], 15.0);
915    }
916
917    #[test]
918    fn test_initial_guess() {
919        let coeffs = vec![10.0, 34.0, 75.0, 94.0, 150.0, 94.0, 75.0, 34.0, 10.0];
920        let guesses = initial_guess(&coeffs);
921
922        assert_eq!(guesses.len(), 4);
923        // Verify the first guess is reasonable
924        assert!(guesses[0].x_.abs() > 0.0);
925        assert!(guesses[0].y_.abs() > 0.0);
926    }
927
928    #[test]
929    fn test_pbairstow_even() {
930        let coeffs = vec![10.0, 34.0, 75.0, 94.0, 150.0, 94.0, 75.0, 34.0, 10.0];
931        let mut vrs = initial_guess(&coeffs);
932        let options = Options::default();
933
934        let (niter, found) = pbairstow_even(&coeffs, &mut vrs, &options);
935
936        assert!(niter > 0);
937        assert!(found);
938        // Verify at least one root is close to actual root
939        let mut has_root = false;
940        for vr in vrs {
941            let val = horner(&mut coeffs.clone(), coeffs.len() - 1, &vr);
942            if val.norm_inf() < options.tolerance {
943                has_root = true;
944                break;
945            }
946        }
947        assert!(has_root);
948    }
949
950    #[test]
951    fn test_initial_autocorr() {
952        let coeffs = vec![10.0, 34.0, 75.0, 94.0, 150.0, 94.0, 75.0, 34.0, 10.0];
953        let guesses = initial_autocorr(&coeffs);
954
955        assert_eq!(guesses.len(), 2);
956        // Verify the first guess is reasonable
957        assert!(guesses[0].x_.abs() > 0.0);
958        assert!(guesses[0].y_.abs() > 0.0);
959    }
960
961    #[test]
962    fn test_extract_autocorr() {
963        let vr = Vector2::new(1.0, -4.0);
964        let result = extract_autocorr(vr);
965
966        assert_approx_eq!(result.x_, 0.25);
967        assert_approx_eq!(result.y_, -0.25);
968    }
969
970    #[test]
971    fn test_pbairstow_autocorr() {
972        let coeffs = vec![10.0, 34.0, 75.0, 94.0, 150.0, 94.0, 75.0, 34.0, 10.0];
973        let mut vrs = initial_autocorr(&coeffs);
974        let options = Options::default();
975
976        let (niter, found) = pbairstow_autocorr(&coeffs, &mut vrs, &options);
977
978        assert!(niter > 0);
979        assert!(found);
980        // Verify at least one root is close to actual root
981        let mut has_root = false;
982        for vr in vrs {
983            let val = horner(&mut coeffs.clone(), coeffs.len() - 1, &vr);
984            if val.norm_inf() < options.tolerance {
985                has_root = true;
986                break;
987            }
988        }
989        assert!(has_root);
990    }
991
992    #[test]
993    fn test_delta1() {
994        let vA = Vector2::new(1.0, 2.0);
995        let vr = Vector2::new(-2.0, -0.0);
996        let vp = Vector2::new(4.0, -5.0);
997        let delta = delta1(&vA, &vr, &vp);
998
999        assert_approx_eq!(delta.x_, 0.2);
1000        assert_approx_eq!(delta.y_, 0.4);
1001    }
1002
1003    #[test]
1004    fn test_roots_from_quadratic_real() {
1005        // x^2 - 3x + 2 = (x-1)(x-2) -> r=3, q=-2
1006        let vr = Vec2::new(3.0, -2.0);
1007        let (r1, r2) = roots_from_quadratic(&vr);
1008        assert!((r1.re - 2.0).abs() < 1e-12);
1009        assert!((r2.re - 1.0).abs() < 1e-12);
1010        assert!(r1.im.abs() < 1e-12);
1011        assert!(r2.im.abs() < 1e-12);
1012    }
1013
1014    #[test]
1015    fn test_roots_from_quadratic_complex() {
1016        // x^2 + 1 = 0 -> r=0, q=-1 (since x^2 - r*x - q = 0) -> roots: i, -i
1017        let vr = Vec2::new(0.0, -1.0);
1018        let (r1, r2) = roots_from_quadratic(&vr);
1019        assert!((r1.re).abs() < 1e-12);
1020        assert!((r1.im - 1.0).abs() < 1e-12);
1021        assert!((r2.re).abs() < 1e-12);
1022        assert!((r2.im + 1.0).abs() < 1e-12);
1023    }
1024
1025    #[test]
1026    fn test_poly_from_quadratic_factors() {
1027        // (x-1)(x-2) = x^2 - 3x + 2 -> r=3, q=-2
1028        let vrs = vec![Vec2::new(3.0, -2.0)];
1029        let coeffs = poly_from_quadratic_factors(&vrs);
1030        assert_eq!(coeffs.len(), 3);
1031        assert!((coeffs[0] - 1.0).abs() < 1e-12);
1032        assert!((coeffs[1] + 3.0).abs() < 1e-12);
1033        assert!((coeffs[2] - 2.0).abs() < 1e-12);
1034    }
1035
1036    #[test]
1037    fn test_poly_from_autocorr_factors() {
1038        let vrs = vec![Vec2::new(3.0, -2.0)];
1039        let coeffs = poly_from_autocorr_factors(&vrs);
1040        // With reciprocals: roots are 2, 1, 0.5, 1.0
1041        // polynomial = (x-2)(x-1)(x-0.5)(x-1) = ...
1042        assert_eq!(coeffs.len(), 5);
1043        assert!((coeffs[0] - 1.0).abs() < 1e-12);
1044    }
1045}