ginger/aberth.rs
1#![allow(non_snake_case)]
2
3use super::horner::{horner_eval_c, horner_eval_f};
4use super::Options;
5use crate::execution_policy::{atomic_decoupled_run, jacobi_mt_run, sequential_run, Step};
6use crate::leja_order::leja_order;
7use crate::scaling::{derivative_coeffs, scaled_coeffs};
8use crate::seqlock::AtomicComplex;
9use num_complex::Complex;
10
11const TWO_PI: f64 = std::f64::consts::TAU;
12
13/// Initial guess for Aberth's method using low-discrepancy sequence
14///
15/// The center $$ c $$ and radius $$ R $$ are derived from the coefficients:
16///
17/// $$ c = -\frac{a_1}{n a_0}, \qquad R = \sqrt\[n\]{-P(c)}, \qquad z_i = c + R \cdot (\cos\theta_i + i\sin\theta_i) $$
18///
19/// where $$ \theta_i = 2\pi v_i $$ with $$ v_i $$ from a van der Corput low-discrepancy sequence.
20pub fn initial_aberth(coeffs: &[f64]) -> Vec<Complex<f64>> {
21 let degree = coeffs.len() - 1;
22 let center = -coeffs[1] / (coeffs[0] * degree as f64);
23 let poly_c = horner_eval_f(coeffs, center);
24 let radius = Complex::<f64>::new(-poly_c, 0.0).powf(1.0 / degree as f64);
25 (0..degree)
26 .map(|i| {
27 // note! swap x and y to match C++ Complex{circle2_table_y, circle2_table_x}
28 let xcoord = crate::tables::circle2_table_y(i);
29 let ycoord = crate::tables::circle2_table_x(i);
30 center + radius * Complex::<f64>::new(xcoord, ycoord)
31 })
32 .collect()
33}
34
35/// Initial guess for Aberth's method
36///
37/// The center $$ c $$ and radius $$ R $$ are derived from the coefficients:
38///
39/// $$ c = -\frac{a_1}{n a_0}, \qquad R = \sqrt\[n\]{-P(c)}, \qquad z_i = c + R \cdot e^{i\theta_i} $$
40///
41/// where $$ \theta_i = 2\pi v_i $$ with $$ v_i $$ from a van der Corput sequence
42/// for low-discrepancy angular spacing.
43///
44/// The `initial_aberth` function calculates the initial guesses for Aberth's method given a
45/// polynomial's coefficients.
46///
47/// Arguments:
48///
49/// * `coeffs`: The `coeffs` parameter is a slice of `f64` values representing the coefficients of a
50/// polynomial. The coefficients are ordered from highest degree to lowest degree. For example, if the
51/// polynomial is `3x^2 + 2x + 1`, the `coeffs` slice would
52///
53/// Returns:
54///
55/// The function `initial_aberth` returns a vector of `Complex<f64>` values, which represent the initial
56/// guesses for the roots of a polynomial.
57///
58/// # Examples:
59///
60/// ```
61/// use ginger::aberth::initial_aberth_orig;
62/// use num_complex::Complex;
63/// use approx_eq::assert_approx_eq;
64///
65/// let coeffs = vec![10.0, 34.0, 75.0, 94.0, 150.0, 94.0, 75.0, 34.0, 10.0];
66/// let z0s = initial_aberth_orig(&coeffs);
67///
68/// assert_approx_eq!(z0s[0].re, 0.6116610247366323);
69/// assert_approx_eq!(z0s[0].im, 0.6926747514925476);
70/// ```
71/// Original initial guess for Aberth's method (equi-angular spacing)
72///
73/// $$ z_i = c + R \cdot \left(\cos\frac{2\pi k}{n} + i\sin\frac{2\pi k}{n}\right) $$
74pub fn initial_aberth_orig(coeffs: &[f64]) -> Vec<Complex<f64>> {
75 let degree = coeffs.len() - 1;
76 let center = -coeffs[1] / (coeffs[0] * degree as f64);
77 let poly_c = horner_eval_f(coeffs, center);
78 let radius = Complex::<f64>::new(-poly_c, 0.0).powf(1.0 / degree as f64);
79 let k = TWO_PI / (degree as f64);
80 (0..degree)
81 .map(|idx| {
82 let theta = k * (0.25 + idx as f64);
83 center + radius * Complex::<f64>::new(theta.cos(), theta.sin())
84 })
85 .collect()
86}
87
88/// One Aberth-Ehrlich correction for a single root.
89///
90/// This mirrors `ginger::detail::aberth_step` in ginger-cpp.
91pub struct AberthStep<'a> {
92 /// Polynomial coefficients (highest degree first).
93 pub coeffs: &'a [f64],
94 /// Derivative coefficients of `coeffs`.
95 pub coeffs1: &'a [f64],
96}
97
98impl Step<Complex<f64>> for AberthStep<'_> {
99 type Cell = AtomicComplex;
100
101 fn run<G, N>(&self, idx: usize, get: G, neighbors: N) -> (f64, Option<Complex<f64>>)
102 where
103 G: Fn(usize) -> Complex<f64>,
104 N: Iterator<Item = usize>,
105 {
106 let zi = get(idx);
107 let p_eval = horner_eval_c(self.coeffs, &zi);
108 let tol_i = p_eval.l1_norm(); // ???
109 let mut p1_eval = horner_eval_c(self.coeffs1, &zi);
110 for j in neighbors {
111 let zj = get(j);
112 p1_eval -= p_eval / (zi - zj);
113 }
114 (tol_i, Some(zi - p_eval / p1_eval)) // Gauss-Seidel fashion
115 }
116}
117
118/// Aberth's method
119///
120/// The `aberth` function implements Aberth's method for finding roots of a polynomial.
121///
122/// Each estimate $$ z_i $$ is updated by the correction:
123///
124/// $$ z_i' = z_i - \frac{P(z_i)}{P'(z_i)} $$
125///
126/// where the derivative is modified by all other root estimates:
127///
128/// $$ P'(z_i) = P_1(z_i) - \sum_{\substack{j=1\\j\neq i}}^n \frac{P(z_i)}{z_i - z_j} $$
129///
130/// Here $$ P_1(z) $$ is the ordinary derivative of $$ P(z) $$. Convergence is cubic.
131///
132/// Arguments:
133///
134/// * `coeffs`: The `coeffs` parameter is a slice of `f64` values representing the coefficients of a
135/// polynomial. The coefficients are ordered from highest degree to lowest degree. For example, if the
136/// polynomial is `3x^2 + 2x + 1`, the `coeffs` slice would
137/// * `zs`: A vector of complex numbers representing the initial guesses for the roots of the polynomial.
138/// * `options`: The `options` parameter is an instance of the `Options` struct, which contains the
139/// following fields:
140///
141/// # Examples:
142///
143/// ```
144/// use ginger::rootfinding::Options;
145/// use ginger::aberth::{initial_aberth, aberth};
146///
147/// let coeffs = vec![10.0, 34.0, 75.0, 94.0, 150.0, 94.0, 75.0, 34.0, 10.0];
148/// let mut zrs = initial_aberth(&coeffs);
149/// let (niter, _found) = aberth(&coeffs, &mut zrs, &Options::default());
150///
151/// assert_eq!(niter, 5);
152/// ```
153#[cfg_attr(feature = "doc-images", doc = svgbobdoc::transform!(
154/// ```svgbob
155/// .───────────. .───────────. .──────────────.
156/// │ Polynomial│───►│ Initial │───►│ Aberth │
157/// │ coeffs │ │ Guess │ │ Iteration │
158/// '───────────' '───────────' '──────┬───────'
159/// │
160/// .────▼───────.
161/// │ Converged? │
162/// '────┬───────'
163/// No │ Yes
164/// .─────────┘ │
165/// ▼ ▼
166/// .───────────. .───────────.
167/// │ Iterate │ │ Roots │
168/// '───────────' '───────────'
169/// ```
170))]
171pub fn aberth(coeffs: &[f64], zs: &mut [Complex<f64>], options: &Options) -> (usize, bool) {
172 let scaled = scaled_coeffs(coeffs);
173 let coeffs1 = derivative_coeffs(&scaled);
174 let step = AberthStep {
175 coeffs: &scaled,
176 coeffs1: &coeffs1,
177 };
178 sequential_run(zs, options, &step)
179}
180
181/// Multi-threading Aberth's method
182///
183/// Each estimate $$ z_i $$ is updated by the correction:
184///
185/// $$ z_i' = z_i - \frac{P(z_i)}{P'(z_i)} $$
186///
187/// where the derivative is modified by all other root estimates:
188///
189/// $$ P'(z_i) = P_1(z_i) - \sum_{\substack{j=1\\j\neq i}}^n \frac{P(z_i)}{z_i - z_j} $$
190///
191/// The `aberth_mt` function in Rust implements the multi-threaded Aberth's method for root finding.
192///
193/// Arguments:
194///
195/// * `coeffs`: The `coeffs` parameter is a slice of `f64` values representing the coefficients of a
196/// polynomial. The polynomial is defined by the equation:
197/// * `zs`: A mutable reference to a vector of Complex numbers. These numbers represent the initial
198/// guesses for the roots of the polynomial equation.
199/// * `options`: The `options` parameter is an instance of the `Options` struct, which contains the
200/// following fields:
201///
202/// # Examples:
203///
204/// ```
205/// use ginger::rootfinding::Options;
206/// use ginger::aberth::{initial_aberth, aberth_mt};
207///
208/// let coeffs = vec![10.0, 34.0, 75.0, 94.0, 150.0, 94.0, 75.0, 34.0, 10.0];
209/// let mut zrs = initial_aberth(&coeffs);
210/// let (niter, _found) = aberth_mt(&coeffs, &mut zrs, &Options::default());
211///
212/// assert_eq!(niter, 6);
213/// ```
214pub fn aberth_mt(coeffs: &[f64], zs: &mut [Complex<f64>], options: &Options) -> (usize, bool) {
215 let scaled = scaled_coeffs(coeffs);
216 let coeffs1 = derivative_coeffs(&scaled);
217 let step = AberthStep {
218 coeffs: &scaled,
219 coeffs1: &coeffs1,
220 };
221 jacobi_mt_run(zs, options, &step)
222}
223
224/// Atomic multi-threading Aberth's method (decoupled)
225///
226/// Each estimate $$ z_i $$ is updated by the correction:
227///
228/// $$ z_i' = z_i - \frac{P(z_i)}{P'(z_i)} $$
229///
230/// where the derivative is modified by all other root estimates:
231///
232/// $$ P'(z_i) = P_1(z_i) - \sum_{\substack{j=1\\j\neq i}}^n \frac{P(z_i)}{z_i - z_j} $$
233///
234/// Unlike `aberth_mt` (Jacobi snapshot + per-iteration barrier), this variant
235/// builds a single atomic working buffer once: each thread owns a chunk of root
236/// slots (single-writer, multi-reader via a seqlock) and runs its own iteration
237/// loop independently. There is no per-iteration synchronization — a thread
238/// exits as soon as its own chunk converges or the maximum number of
239/// iterations is exceeded. The returned iteration count is the maximum across
240/// threads, and `found` is true only if every chunk converged.
241///
242/// Arguments:
243///
244/// * `coeffs`: The `coeffs` parameter is a slice of `f64` values representing the coefficients of a
245/// polynomial. The polynomial is defined by the equation:
246/// * `zs`: A mutable reference to a vector of Complex numbers. These numbers represent the initial
247/// guesses for the roots of the polynomial equation.
248/// * `options`: The `options` parameter is an instance of the `Options` struct, which contains the
249/// following fields:
250///
251/// # Examples:
252///
253/// ```
254/// use ginger::rootfinding::Options;
255/// use ginger::aberth::{initial_aberth, aberth_atomic};
256///
257/// let coeffs = vec![10.0, 34.0, 75.0, 94.0, 150.0, 94.0, 75.0, 34.0, 10.0];
258/// let mut zrs = initial_aberth(&coeffs);
259/// let (niter, _found) = aberth_atomic(&coeffs, &mut zrs, &Options::default());
260///
261/// assert!(niter > 0);
262/// ```
263pub fn aberth_atomic(coeffs: &[f64], zs: &mut [Complex<f64>], options: &Options) -> (usize, bool) {
264 let scaled = scaled_coeffs(coeffs);
265 let coeffs1 = derivative_coeffs(&scaled);
266 let step = AberthStep {
267 coeffs: &scaled,
268 coeffs1: &coeffs1,
269 };
270 atomic_decoupled_run(zs, options, &step)
271}
272
273/// Initial guess for Aberth's method using auto-correlation
274///
275/// $$ R = \sqrt\[n\]{|a_n|},\qquad R \leftarrow \max(R, 1/R),\qquad z_i = c + R \cdot e^{i\theta_i} $$
276///
277/// The `initial_aberth_autocorr` function calculates initial guesses for Aberth's method
278/// specifically tailored for auto-correlation polynomials.
279///
280/// Arguments:
281///
282/// * `coeffs`: The `coeffs` parameter is a slice of `f64` values representing the coefficients
283/// of a polynomial. The coefficients are ordered from highest degree to lowest degree.
284///
285/// Returns:
286///
287/// The function returns a vector of `Complex<f64>` values, representing the initial guesses
288/// for the roots of a polynomial.
289pub fn initial_aberth_autocorr(coeffs: &[f64]) -> Vec<Complex<f64>> {
290 let degree = coeffs.len() - 1; // assume even
291 let center = -coeffs[1] / (coeffs[0] * degree as f64);
292 let poly_c = horner_eval_f(coeffs, center);
293 let mut radius = poly_c.abs().powf(1.0 / degree as f64);
294 if radius > 1.0 {
295 radius = 1.0 / radius;
296 }
297 (0..degree / 2)
298 .map(|i| {
299 // note! swap x and y to match C++ Complex{circle2_table_y, circle2_table_x}
300 let xcoord = crate::tables::circle2_table_y(i);
301 let ycoord = crate::tables::circle2_table_x(i);
302 center + radius * Complex::<f64>::new(xcoord, ycoord)
303 })
304 .collect()
305}
306
307/// One Aberth-Ehrlich correction for a palindromic (auto-correlation) root;
308/// each neighbor root contributes both `zj` and its reciprocal `1/zj`.
309///
310/// This mirrors `ginger::detail::aberth_autocorr_step` in ginger-cpp.
311pub struct AberthAutocorrStep<'a> {
312 /// Polynomial coefficients (highest degree first).
313 pub coeffs: &'a [f64],
314 /// Derivative coefficients of `coeffs`.
315 pub coeffs1: &'a [f64],
316}
317
318impl Step<Complex<f64>> for AberthAutocorrStep<'_> {
319 type Cell = AtomicComplex;
320
321 fn run<G, N>(&self, idx: usize, get: G, neighbors: N) -> (f64, Option<Complex<f64>>)
322 where
323 G: Fn(usize) -> Complex<f64>,
324 N: Iterator<Item = usize>,
325 {
326 let zi = get(idx);
327 let p_eval = horner_eval_c(self.coeffs, &zi);
328 let tol_i = p_eval.l1_norm(); // ???
329 let mut p1_eval = horner_eval_c(self.coeffs1, &zi);
330 for j in neighbors {
331 let zj = get(j);
332 p1_eval -= p_eval / (zi - zj);
333 p1_eval -= p_eval / (zi - 1.0 / zj);
334 }
335 (tol_i, Some(zi - p_eval / p1_eval)) // Gauss-Seidel fashion
336 }
337}
338
339/// Aberth's method for auto-correlation polynomials
340///
341/// Each estimate $$ z_i $$ is updated by the correction, considering both $$ z $$
342/// and $$ 1/\bar{z} $$ in the derivative:
343///
344/// $$ P'(z_i) = P_1(z_i) - \sum_{\substack{j=1\\j\neq i}}^n
345/// \left(\frac{P(z_i)}{z_i - z_j} + \frac{P(z_i)}{z_i - 1/z_j}\right) $$
346///
347/// The `aberth_autocorr` function implements Aberth's method specifically for
348/// auto-correlation polynomials, where roots come in reciprocal pairs.
349///
350/// Arguments:
351///
352/// * `coeffs`: The polynomial coefficients (highest to lowest degree)
353/// * `zs`: Mutable slice of complex root approximations
354/// * `options`: Iteration options (max iterations, tolerance)
355///
356/// Returns:
357///
358/// A tuple of (number of iterations, whether convergence was achieved)
359pub fn aberth_autocorr(
360 coeffs: &[f64],
361 zs: &mut [Complex<f64>],
362 options: &Options,
363) -> (usize, bool) {
364 let scaled = scaled_coeffs(coeffs);
365 let coeffs1 = derivative_coeffs(&scaled);
366 let step = AberthAutocorrStep {
367 coeffs: &scaled,
368 coeffs1: &coeffs1,
369 };
370 sequential_run(zs, options, &step)
371}
372
373/// Reconstruct a monic polynomial from its roots using Leja ordering
374///
375/// $$ P(x) = \prod_{i=1}^n (x - r_i) = x^n + a_{n-1} x^{n-1} + \cdots + a_0 $$
376///
377/// The coefficients are computed by repeated convolution. Starting from $$ \[1\] $$,
378/// for each root $$ r $$:
379///
380/// $$ c_{k+1} \leftarrow c_{k+1} - r \cdot c_k $$
381///
382/// Given a set of complex roots, reconstruct the monic polynomial coefficients
383/// (highest degree first) by multiplying (x - root) factors. Leja ordering is
384/// applied for numerical accuracy.
385///
386/// Arguments:
387///
388/// * `zs` - Input vector of complex roots
389///
390/// Returns:
391///
392/// Monic polynomial coefficients (highest degree first)
393pub fn poly_from_roots(zs: &[Complex<f64>]) -> Vec<f64> {
394 if zs.is_empty() {
395 return vec![1.0];
396 }
397 let ordered = leja_order(zs.to_vec());
398 let mut coeffs = vec![Complex::new(1.0, 0.0)];
399 for z in &ordered {
400 let mut prev = coeffs[0];
401 for item in coeffs.iter_mut().skip(1) {
402 let old = *item;
403 *item -= z * prev;
404 prev = old;
405 }
406 coeffs.push(-z * prev);
407 }
408 coeffs.iter().map(|c| c.re).collect()
409}
410
411/// Reconstruct a monic polynomial from its autocorrelation roots
412///
413/// Auto-correlation (palindromic) polynomials have roots in reciprocal pairs.
414/// The `aberth_autocorr` functions find the degree/2 "independent" roots.
415/// This function adds the reciprocal of each root $1/z$ to get the full set
416/// of degree roots, then reconstructs with Leja ordering.
417///
418/// $$ P(x) = \prod_{i=1}^{n/2} (x - z_i)(x - 1/z_i) $$
419///
420/// Arguments:
421///
422/// * `zs` - Roots found by aberth_autocorr
423///
424/// Returns:
425///
426/// Monic polynomial coefficients (highest degree first)
427pub fn poly_from_autocorr_roots(zs: &[Complex<f64>]) -> Vec<f64> {
428 if zs.is_empty() {
429 return vec![1.0];
430 }
431 // Add reciprocals to account for the palindromic root-pair structure
432 let mut all_roots: Vec<Complex<f64>> = Vec::with_capacity(2 * zs.len());
433 for z in zs {
434 all_roots.push(*z);
435 all_roots.push(1.0 / z);
436 }
437 poly_from_roots(&all_roots)
438}
439
440#[cfg(test)]
441mod tests {
442 use super::*;
443
444 #[test]
445 fn test_horner_eval() {
446 let coeffs = vec![10.0, 34.0, 75.0, 94.0, 150.0, 94.0, 75.0, 34.0, 10.0];
447 let z = Complex::new(0.0, 0.0);
448 let p_eval = horner_eval_c(&coeffs, &z);
449 assert_eq!(p_eval.re, 10.0);
450 assert_eq!(p_eval.im, 0.0);
451 let z = Complex::new(1.0, 0.0);
452 let p_eval = horner_eval_c(&coeffs, &z);
453 assert_eq!(p_eval.re, 576.0);
454 assert_eq!(p_eval.im, 0.0);
455 }
456
457 #[test]
458 fn test_aberth() {
459 let coeffs = vec![10.0, 34.0, 75.0, 94.0, 150.0, 94.0, 75.0, 34.0, 10.0];
460 let mut zrs = initial_aberth(&coeffs);
461 let (niter, found) = aberth(&coeffs, &mut zrs, &Options::default());
462 assert_eq!(niter, 5);
463 assert!(found);
464 }
465
466 #[test]
467 fn test_aberth_mt() {
468 let coeffs = vec![10.0, 34.0, 75.0, 94.0, 150.0, 94.0, 75.0, 34.0, 10.0];
469 let mut zrs = initial_aberth(&coeffs);
470 let (niter, found) = aberth_mt(&coeffs, &mut zrs, &Options::default());
471 assert_eq!(niter, 6);
472 assert!(found);
473 }
474
475 #[test]
476 fn test_aberth_atomic() {
477 let coeffs = vec![10.0, 34.0, 75.0, 94.0, 150.0, 94.0, 75.0, 34.0, 10.0];
478 let mut zrs = initial_aberth(&coeffs);
479 let (niter, found) = aberth_atomic(&coeffs, &mut zrs, &Options::default());
480 assert!(found);
481 assert!(niter <= 14);
482 }
483
484 const FIR_COEFFS: [f64; 49] = [
485 -0.00196191,
486 -0.00094597,
487 -0.00023823,
488 0.00134667,
489 0.00380494,
490 0.00681596,
491 0.0097864,
492 0.01186197,
493 0.0121238,
494 0.00985211,
495 0.00474894,
496 -0.00281751,
497 -0.01173923,
498 -0.0201885,
499 -0.02590168,
500 -0.02658216,
501 -0.02035729,
502 -0.00628271,
503 0.01534627,
504 0.04279982,
505 0.0732094,
506 0.10275561,
507 0.12753013,
508 0.14399228,
509 0.15265722,
510 0.14399228,
511 0.12753013,
512 0.10275561,
513 0.0732094,
514 0.04279982,
515 0.01534627,
516 -0.00628271,
517 -0.02035729,
518 -0.02658216,
519 -0.02590168,
520 -0.0201885,
521 -0.01173923,
522 -0.00281751,
523 0.00474894,
524 0.00985211,
525 0.0121238,
526 0.01186197,
527 0.0097864,
528 0.00681596,
529 0.00380494,
530 0.00134667,
531 -0.00023823,
532 -0.00094597,
533 -0.00196191,
534 ];
535
536 #[test]
537 fn test_aberth_atomic_fir() {
538 let options = Options {
539 tolerance: 1e-8,
540 ..Options::default()
541 };
542 let mut zrs = initial_aberth(&FIR_COEFFS);
543 let (niter, found) = aberth_atomic(&FIR_COEFFS, &mut zrs, &options);
544 assert!(found);
545 assert!(niter <= 100, "niter={niter}");
546 }
547
548 #[test]
549 fn test_aberth_atomic_reconstruction() {
550 let coeffs = vec![10.0, 34.0, 75.0, 94.0, 150.0, 94.0, 75.0, 34.0, 10.0];
551 let mut zrs = initial_aberth(&coeffs);
552 let (_, found) = aberth_atomic(&coeffs, &mut zrs, &Options::default());
553 assert!(found);
554 let monic = poly_from_roots(&zrs);
555 let scale = coeffs[0];
556 for (i, c) in coeffs.iter().enumerate() {
557 assert!(
558 (monic[i] * scale - c).abs() < 1e-8,
559 "coefficient {i} mismatch: {} vs {}",
560 monic[i] * scale,
561 c
562 );
563 }
564 }
565
566 #[test]
567 fn test_aberth_autocorr() {
568 let coeffs = vec![10.0, 34.0, 75.0, 94.0, 150.0, 94.0, 75.0, 34.0, 10.0];
569 let mut zrs = initial_aberth_autocorr(&coeffs);
570 let (niter, found) = aberth_autocorr(&coeffs, &mut zrs, &Options::default());
571 assert!(niter <= 7);
572 assert!(found);
573 }
574
575 #[test]
576 fn test_aberth_scaled_polynomial_converges() {
577 // Same roots as the degree-8 palindromic polynomial, but with
578 // coefficients large enough that an absolute residual test can never
579 // reach the default 1e-12 tolerance.
580 let base = vec![10.0, 34.0, 75.0, 94.0, 150.0, 94.0, 75.0, 34.0, 10.0];
581 let scaled: Vec<f64> = base.iter().map(|c| c * 1e6).collect();
582 let mut zs = initial_aberth(&scaled);
583 let (niter, found) = aberth(&scaled, &mut zs, &Options::default());
584 assert!(found);
585 assert!(niter <= 12, "niter={niter}");
586 }
587
588 #[test]
589 fn test_scaling_does_not_change_roots() {
590 let base = vec![10.0, 34.0, 75.0, 94.0, 150.0, 94.0, 75.0, 34.0, 10.0];
591 let scaled: Vec<f64> = base.iter().map(|c| c * 1e6).collect();
592 let z0 = initial_aberth(&base);
593 let mut za = z0.clone();
594 let mut zb = z0.clone();
595 let (na, oka) = aberth(&base, &mut za, &Options::default());
596 let (nb, okb) = aberth(&scaled, &mut zb, &Options::default());
597 assert!(oka && okb);
598 assert_eq!(na, nb);
599 for (a, b) in za.iter().zip(zb.iter()) {
600 assert!((a - b).norm() < 1e-9, "{a} vs {b}");
601 }
602 }
603
604 #[test]
605 fn test_poly_from_roots() {
606 // Polynomial (x-1)(x-2) = x^2 - 3x + 2
607 let roots = vec![Complex::new(1.0, 0.0), Complex::new(2.0, 0.0)];
608 let coeffs = poly_from_roots(&roots);
609 assert_eq!(coeffs.len(), 3);
610 assert!((coeffs[0] - 1.0).abs() < 1e-12);
611 assert!((coeffs[1] + 3.0).abs() < 1e-12);
612 assert!((coeffs[2] - 2.0).abs() < 1e-12);
613 }
614
615 #[test]
616 fn test_poly_from_autocorr_roots() {
617 // Simple test: just check it doesn't panic and returns right length
618 let roots = vec![Complex::new(0.5, 0.5), Complex::new(0.5, -0.5)];
619 let coeffs = poly_from_autocorr_roots(&roots);
620 // Should have 2*roots.len() + 1 coefficients
621 assert_eq!(coeffs.len(), 5);
622 // Monic
623 assert!((coeffs[0] - 1.0).abs() < 1e-12);
624 }
625
626 #[test]
627 fn test_poly_from_roots_empty() {
628 let coeffs = poly_from_roots(&[]);
629 assert_eq!(coeffs, vec![1.0]);
630 }
631}