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}