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