Skip to main content

feanor_math/algorithms/
newton.rs

1use std::cmp::Ordering;
2
3use oorandom::Rand64;
4
5use crate::MAX_PROBABILISTIC_REPETITIONS;
6use crate::algorithms::poly_gcd::squarefree_part::poly_squarefree_part_local;
7use crate::computation::DontObserve;
8use crate::divisibility::{DivisibilityRing, DivisibilityRingStore};
9use crate::field::FieldStore;
10use crate::homomorphism::*;
11use crate::integer::*;
12use crate::ring::*;
13use crate::rings::float_complex::{Complex64, Complex64Base, Complex64El};
14use crate::rings::poly::dense_poly::*;
15use crate::rings::poly::*;
16
17const NEWTON_MAX_SCALE: u32 = 10;
18const NEWTON_ITERATIONS: usize = 16;
19const ASSUME_RADIUS_TO_APPROX_RADIUS_FACTOR: f64 = 2.0;
20
21#[derive(Debug)]
22#[stability::unstable(feature = "enable")]
23pub struct PrecisionError;
24
25#[stability::unstable(feature = "enable")]
26pub fn absolute_error_of_poly_eval<P>(
27    poly_ring: P,
28    f: &El<P>,
29    poly_deg: usize,
30    point: Complex64El,
31    relative_error_point: f64,
32) -> f64
33where
34    P: RingStore,
35    P::Type: PolyRing + DivisibilityRing,
36    <P::Type as RingExtension>::BaseRing: RingStore<Type = Complex64Base>,
37{
38    let CC = Complex64::RING;
39    let mut current = point;
40    let mut current_relative_error = relative_error_point;
41    let mut total_error = 0.0;
42    for i in 1..=poly_deg {
43        total_error += CC.abs(*poly_ring.coefficient_at(f, i)) * CC.abs(current) * current_relative_error;
44        // technically, we would have `(1 + current_relative_error)(1 + f64::EPSILON) - 1`, but we
45        // ignore `O(f64::EPSILON^2)` terms here
46        current_relative_error += f64::EPSILON;
47        CC.mul_assign(&mut current, point);
48    }
49    return total_error;
50}
51
52fn bound_distance_to_root<P>(
53    approx_root: Complex64El,
54    CCX: P,
55    poly: &El<P>,
56    poly_deg: usize,
57) -> Result<f64, PrecisionError>
58where
59    P: RingStore,
60    P::Type: PolyRing + DivisibilityRing,
61    <P::Type as RingExtension>::BaseRing: RingStore<Type = Complex64Base>,
62{
63    let CC = Complex64::RING;
64    let f = poly;
65    let f_prime = derive_poly(&CCX, f);
66
67    let approx_radius = (CC.abs(CCX.evaluate(f, &approx_root, CC.identity()))
68        + absolute_error_of_poly_eval(&CCX, f, poly_deg, approx_root, 0.0))
69        / CC.abs(CCX.evaluate(&f_prime, &approx_root, CC.identity()));
70    if !approx_radius.is_finite() {
71        return Err(PrecisionError);
72    }
73
74    let assume_radius = approx_radius * ASSUME_RADIUS_TO_APPROX_RADIUS_FACTOR;
75    // we bound `|f'(x + t) - f'(x)| <= epsilon` for `t` at most `assume_radius` using the taylor
76    // series
77    let mut abs_taylor_series_coeffs = Vec::new();
78    let mut current = derive_poly(&CCX, &f_prime);
79    for i in 0..poly_deg.saturating_sub(1) {
80        CCX.inclusion()
81            .mul_assign_map(&mut current, CC.from_f64(1.0 / (i as f64 + 1.0)));
82        abs_taylor_series_coeffs.push(CC.abs(CCX.evaluate(&current, &approx_root, CC.identity())));
83        current = derive_poly(&CCX, &current);
84    }
85    let f_prime_bound = abs_taylor_series_coeffs
86        .iter()
87        .enumerate()
88        .map(|(i, c)| c * assume_radius.powi(i as i32 + 1))
89        .sum::<f64>();
90
91    // The idea is as follows: We have `f(x + t) = f(x) + f'(g(t)) t` where `g(t)` is a value
92    // between `x` and `x + t`; Using `|f'(g(t))| >= |f'(x)| - f_prime_bound` and rearranging it
93    // gives `t <= |f(x)| / (|f'(x)| - f_prime_bound)`; For this to be at most `assume_radius`,
94    // it suffices to assume `f_prime_bound <= |f'(x)| - |f(x)|/R = |f'(x)| (1 - approx_radius /
95    // assume_radius)`
96    if f_prime_bound
97        > CC.abs(CCX.evaluate(&f_prime, &approx_root, CC.identity())) * (ASSUME_RADIUS_TO_APPROX_RADIUS_FACTOR - 1.0)
98            / ASSUME_RADIUS_TO_APPROX_RADIUS_FACTOR
99    {
100        return Err(PrecisionError);
101    }
102
103    return Ok(assume_radius);
104}
105
106fn newton_with_initial<P>(
107    poly_ring: P,
108    f: &El<P>,
109    poly_deg: usize,
110    initial: El<Complex64>,
111) -> Result<(El<Complex64>, f64), PrecisionError>
112where
113    P: RingStore,
114    P::Type: PolyRing + DivisibilityRing,
115    <P::Type as RingExtension>::BaseRing: RingStore<Type = Complex64Base>,
116{
117    let CC = Complex64::RING;
118    let f_prime = derive_poly(&poly_ring, f);
119    let mut current = initial;
120    for _ in 0..NEWTON_ITERATIONS {
121        current = CC.sub(
122            current,
123            CC.div(
124                &poly_ring.evaluate(f, &current, CC.identity()),
125                &poly_ring.evaluate(&f_prime, &current, CC.identity()),
126            ),
127        );
128    }
129    return Ok((current, bound_distance_to_root(current, poly_ring, f, poly_deg)?));
130}
131
132fn find_approximate_complex_root_squarefree<P>(
133    poly_ring: P,
134    f: &El<P>,
135    poly_deg: usize,
136) -> Result<(El<Complex64>, f64), PrecisionError>
137where
138    P: RingStore,
139    P::Type: PolyRing + DivisibilityRing,
140    <P::Type as RingExtension>::BaseRing: RingStore<Type = Complex64Base>,
141{
142    let mut rng = Rand64::new(1);
143    let CC = Complex64::RING;
144    assert!(
145        poly_ring
146            .terms(f)
147            .all(|(c, _): (&Complex64El, _)| CC.re(*c).is_finite() && CC.im(*c).is_finite())
148    );
149    let f_prime = derive_poly(&poly_ring, f);
150
151    let (approx_root, approx_radius) = (0..MAX_PROBABILISTIC_REPETITIONS)
152        .map(|_| {
153            let starting_point_unscaled = CC.add(
154                // this cast might wrap around i64::MAX, so also produces negative values
155                CC.from_f64((rng.rand_u64() as i64) as f64 / i64::MAX as f64),
156                CC.mul(
157                    Complex64::I,
158                    CC.from_f64((rng.rand_u64() as i64) as f64 / i64::MAX as f64),
159                ),
160            );
161            let scale = (rng.rand_u64() % (2 * NEWTON_MAX_SCALE as u64)) as i32 - NEWTON_MAX_SCALE as i32;
162            let starting_point = CC.mul(starting_point_unscaled, CC.from_f64(2.0f64.powi(scale)));
163
164            let mut current = starting_point;
165            for _ in 0..NEWTON_ITERATIONS {
166                current = CC.sub(
167                    current,
168                    CC.div(
169                        &poly_ring.evaluate(f, &current, CC.identity()),
170                        &poly_ring.evaluate(&f_prime, &current, CC.identity()),
171                    ),
172                );
173            }
174
175            // we expect the root to lie within this radius of current
176            let approx_radius = CC.abs(poly_ring.evaluate(f, &current, CC.identity()))
177                / CC.abs(poly_ring.evaluate(&f_prime, &current, CC.identity()));
178
179            return (current, approx_radius);
180        })
181        .min_by(|(_, r1), (_, r2)| match (r1.is_finite(), r2.is_finite()) {
182            (false, false) => Ordering::Equal,
183            (true, false) => Ordering::Less,
184            (false, true) => Ordering::Greater,
185            (true, true) => f64::total_cmp(r1, r2),
186        })
187        .unwrap();
188    if !approx_radius.is_finite() || approx_radius < 0.0 {
189        return Err(PrecisionError);
190    }
191
192    return Ok((
193        approx_root,
194        bound_distance_to_root(approx_root, poly_ring, f, poly_deg)?,
195    ));
196}
197
198/// Finds an approximation to a complex root of the given integer polynomial.
199///
200/// This function does not try to be as efficient as possible, but instead tries
201/// to avoid (or at least detect) as many numerical problems as possible.
202///
203/// The first return value is an approximation to the root of the polynomial, and the
204/// second return value is an upper bound to the distance to the exact root.
205#[stability::unstable(feature = "enable")]
206pub fn find_approximate_complex_root<P>(ZZX: P, f: &El<P>) -> Result<(El<Complex64>, f64), PrecisionError>
207where
208    P: RingStore,
209    P::Type: PolyRing + DivisibilityRing,
210    <<P::Type as RingExtension>::BaseRing as RingStore>::Type: IntegerRing,
211{
212    assert!(ZZX.degree(f).unwrap_or(0) > 0);
213    let CC = Complex64::RING;
214    assert!(
215        ZZX.checked_div(&poly_squarefree_part_local(&ZZX, ZZX.clone_el(f), DontObserve), f)
216            .is_some(),
217        "polynomial must be square-free"
218    );
219    let CCX = DensePolyRing::new(CC, "X");
220    return find_approximate_complex_root_squarefree(
221        &CCX,
222        &CCX.lifted_hom(&ZZX, CC.can_hom(ZZX.base_ring()).unwrap()).map_ref(f),
223        ZZX.degree(f).unwrap(),
224    );
225}
226
227/// Finds an approximation to all complex roots of the given integer polynomial.
228///
229/// This function does not try to be as efficient as possible, but instead tries
230/// to avoid (or at least detect) as many numerical problems as possible.
231/// However, the task of finding all roots has quite bad numerical stability, especially
232/// if some of the roots are close together. Hence, this is likely to fail for polynomials
233/// with large degrees (say > 100) or very large coefficients.
234///
235/// The first component of each returned tuple is an approximation to a root of the
236/// polynomial, and the second component is an upper bound to the distance to exact root.
237#[stability::unstable(feature = "enable")]
238pub fn find_all_approximate_complex_roots<P>(ZZX: P, poly: &El<P>) -> Result<Vec<(El<Complex64>, f64)>, PrecisionError>
239where
240    P: RingStore,
241    P::Type: PolyRing + DivisibilityRing,
242    <<P::Type as RingExtension>::BaseRing as RingStore>::Type: IntegerRing,
243{
244    assert!(ZZX.degree(poly).unwrap_or(0) > 0);
245    let CC = Complex64::RING;
246    let ZZ_to_CC = CC.can_hom(ZZX.base_ring()).unwrap();
247    assert!(
248        ZZX.checked_div(&poly_squarefree_part_local(&ZZX, ZZX.clone_el(poly), DontObserve), poly)
249            .is_some(),
250        "polynomial must be square-free"
251    );
252    let CCX = DensePolyRing::new(CC, "X");
253    let ZZX_to_CCX = CCX.lifted_hom(&ZZX, ZZ_to_CC);
254
255    let d = ZZX.degree(poly).unwrap();
256    let f = ZZX_to_CCX.map_ref(poly);
257    let mut remaining_poly = CCX.clone_el(&f);
258    let mut result = Vec::new();
259    for i in 0..ZZX.degree(poly).unwrap() {
260        let (next_root_initial, _) = find_approximate_complex_root_squarefree(&CCX, &remaining_poly, d - i)?;
261        let (next_root, distance) = newton_with_initial(&CCX, &f, d, next_root_initial)?;
262        if result
263            .iter()
264            .any(|(prev_root, prev_distance)| CC.abs(CC.sub(*prev_root, next_root)) <= distance + prev_distance)
265        {
266            return Err(PrecisionError);
267        }
268        result.push((next_root, distance));
269        let mut new_remaining_poly = CCX.zero();
270        for j in (1..=(d - i)).rev() {
271            let lc = *CCX.coefficient_at(&remaining_poly, j);
272            CCX.get_ring()
273                .add_assign_from_terms(&mut new_remaining_poly, [(lc, j - 1)]);
274            CCX.get_ring()
275                .add_assign_from_terms(&mut remaining_poly, [(CC.mul(lc, next_root), j - 1)]);
276        }
277        remaining_poly = new_remaining_poly;
278    }
279    // just some canonical order to make tests and debugging easier
280    result.sort_unstable_by(|(l, _), (r, _)| {
281        f64::total_cmp(&(CC.re(*l) + CC.im(*l) * 0.000001), &(CC.re(*r) + CC.im(*r) * 0.000001))
282    });
283    return Ok(result);
284}
285
286#[cfg(test)]
287use std::f64::consts::PI;
288
289#[cfg(test)]
290use crate::algorithms::cyclotomic::cyclotomic_polynomial;
291#[cfg(test)]
292use crate::algorithms::eea::signed_gcd;
293#[cfg(test)]
294use crate::primitive_int::StaticRing;
295
296#[test]
297fn test_find_approximate_complex_root() {
298    let ZZ = BigIntRing::RING;
299    let ZZX = DensePolyRing::new(&ZZ, "X");
300    let CC = Complex64::RING;
301
302    let [f] = ZZX.with_wrapped_indeterminate(|X| [X.pow_ref(2) + 1]);
303    let (root, radius) = find_approximate_complex_root(&ZZX, &f).unwrap();
304    assert!(radius <= 0.000000001);
305    assert!(CC.abs(CC.sub(Complex64::I, root)) <= radius || CC.abs(CC.add(Complex64::I, root)) <= radius);
306
307    let [f] = ZZX.with_wrapped_indeterminate(|X| [100000000 * X.pow_ref(2) + 1]);
308    let (root, radius) = find_approximate_complex_root(&ZZX, &f).unwrap();
309    assert!(radius <= 0.000000001);
310    assert!(
311        CC.abs(CC.sub(CC.mul(Complex64::I, CC.from_f64(0.0001)), root)) <= radius
312            || CC.abs(CC.add(CC.mul(Complex64::I, CC.from_f64(0.0001)), root)) <= radius
313    );
314
315    let f = cyclotomic_polynomial(&ZZX, 105);
316    let (root, radius) = find_approximate_complex_root(&ZZX, &f).unwrap();
317    assert!(radius <= 0.000000001);
318    let root_of_unity = |k, n| CC.exp(CC.mul(CC.from_f64(2.0 * PI * k as f64 / n as f64), Complex64::I));
319    assert!(
320        (0..105)
321            .filter(|k| signed_gcd(*k, 105, StaticRing::<i64>::RING) == 1)
322            .any(|k| CC.abs(CC.sub(root_of_unity(k, 105), root)) <= radius)
323    );
324
325    let [f] = ZZX.with_wrapped_indeterminate(|X| [X.pow_ref(4) - 6 * X.pow_ref(2) - 11]);
326    let (root, radius) = find_approximate_complex_root(&ZZX, &f).unwrap();
327    assert!(radius <= 0.000000001);
328    let expected = [
329        CC.from_f64(-2.7335207983477241859817441249894389670263693372196541057936155022),
330        CC.from_f64(2.7335207983477241859817441249894389670263693372196541057936155022),
331        CC.mul(
332            CC.from_f64(-1.2133160985495821701841076107103126272177604441592580488679327999),
333            Complex64::I,
334        ),
335        CC.mul(
336            CC.from_f64(1.2133160985495821701841076107103126272177604441592580488679327999),
337            Complex64::I,
338        ),
339    ];
340    assert!(expected.iter().any(|x| CC.abs(CC.sub(*x, root)) <= radius));
341}
342
343#[test]
344fn test_find_all_approximate_complex_roots() {
345    let ZZ = BigIntRing::RING;
346    let ZZX = DensePolyRing::new(&ZZ, "X");
347    let CC = Complex64::RING;
348
349    let [f] = ZZX.with_wrapped_indeterminate(|X| [X.pow_ref(3) - 7 * X + 100]);
350    let expected = [
351        CC.from_f64(-5.1425347350362689414102462079341122827700721701123021518405977272),
352        CC.add(
353            CC.from_f64(2.5712673675181344707051231039670561413850360850561510759202988636),
354            CC.mul(
355                CC.from_f64(-3.5824918179656616775077885147170618458138064112538190024361795659),
356                Complex64::I,
357            ),
358        ),
359        CC.add(
360            CC.from_f64(2.5712673675181344707051231039670561413850360850561510759202988636),
361            CC.mul(
362                CC.from_f64(3.5824918179656616775077885147170618458138064112538190024361795659),
363                Complex64::I,
364            ),
365        ),
366    ];
367    let actual = find_all_approximate_complex_roots(&ZZX, &f).unwrap();
368    for (expected, (actual, dist)) in expected.iter().copied().zip(actual.iter().copied()) {
369        assert!(dist < 0.000000001);
370        assert!(CC.abs(CC.sub(actual, expected)) <= dist);
371    }
372
373    let root_of_unity = |k, n| CC.exp(CC.mul(CC.from_f64(2.0 * PI * k as f64 / n as f64), Complex64::I));
374    let f = cyclotomic_polynomial(&ZZX, 105);
375    let expected = (1..=52)
376        .rev()
377        .filter(|i| signed_gcd(*i, 105, StaticRing::<i64>::RING) == 1)
378        .flat_map(|i| [root_of_unity(105 - i, 105), root_of_unity(i, 105)])
379        .chain([CC.one()]);
380    let actual = find_all_approximate_complex_roots(&ZZX, &f).unwrap();
381    assert_eq!(48, actual.len());
382    for (expected, (actual, dist)) in expected.zip(actual.iter().copied()) {
383        assert!(dist < 0.000000001);
384        assert!(CC.abs(CC.sub(actual, expected)) <= dist);
385    }
386}