Skip to main content

ginger/
rootfinding.rs

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