Skip to main content

logicaffeine_base/
complex_algebraic.rs

1//! Complex algebraic numbers `re + im·i` with real algebraic parts, and every complex root of a
2//! polynomial with rational coefficients.
3//!
4//! A non-real root `z = a + b·i` of an irreducible `g` has its conjugate as a root too, so `2a =
5//! z + z̄` is a real root of `S(x) = Res_y(g(y), g(x − y))`, whose roots are the sums of two roots
6//! of `g`; and `z − z̄ = 2b·i`, so `−4b²` is a negative real root of `E`, where `E(x²) =
7//! Res_y(g(y), g(x + y)) / x^deg g` has the nonzero differences of roots for roots. A
8//! floating-point approximation of each root orders the candidates nearest first, and a candidate
9//! pair is accepted only once `g(a + b·i) = 0` is checked exactly — the approximation guides the
10//! search and never decides it.
11
12use std::fmt;
13
14use crate::algebraic::{resultant_in_y, Algebraic};
15use crate::numeric::Rational;
16use crate::poly::Poly;
17
18/// `re + im·i` with real algebraic parts.
19#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
20pub struct ComplexAlgebraic {
21    pub re: Algebraic,
22    pub im: Algebraic,
23}
24
25impl ComplexAlgebraic {
26    pub fn new(re: Algebraic, im: Algebraic) -> ComplexAlgebraic {
27        ComplexAlgebraic { re, im }
28    }
29
30    pub fn real(re: Algebraic) -> ComplexAlgebraic {
31        ComplexAlgebraic {
32            re,
33            im: Algebraic::from_i64(0),
34        }
35    }
36
37    pub fn imag(im: Algebraic) -> ComplexAlgebraic {
38        ComplexAlgebraic {
39            re: Algebraic::from_i64(0),
40            im,
41        }
42    }
43
44    pub fn from_rational(r: Rational) -> ComplexAlgebraic {
45        ComplexAlgebraic::real(Algebraic::from_rational(r))
46    }
47
48    pub fn from_i64(n: i64) -> ComplexAlgebraic {
49        ComplexAlgebraic::real(Algebraic::from_i64(n))
50    }
51
52    pub fn zero() -> ComplexAlgebraic {
53        ComplexAlgebraic::from_i64(0)
54    }
55
56    pub fn one() -> ComplexAlgebraic {
57        ComplexAlgebraic::from_i64(1)
58    }
59
60    pub fn is_zero(&self) -> bool {
61        self.re.is_zero() && self.im.is_zero()
62    }
63
64    pub fn is_real(&self) -> bool {
65        self.im.is_zero()
66    }
67
68    pub fn add(&self, o: &ComplexAlgebraic) -> ComplexAlgebraic {
69        ComplexAlgebraic {
70            re: self.re.add(&o.re),
71            im: self.im.add(&o.im),
72        }
73    }
74
75    pub fn sub(&self, o: &ComplexAlgebraic) -> ComplexAlgebraic {
76        self.add(&o.neg())
77    }
78
79    pub fn neg(&self) -> ComplexAlgebraic {
80        ComplexAlgebraic {
81            re: self.re.neg(),
82            im: self.im.neg(),
83        }
84    }
85
86    pub fn conj(&self) -> ComplexAlgebraic {
87        ComplexAlgebraic {
88            re: self.re.clone(),
89            im: self.im.neg(),
90        }
91    }
92
93    pub fn mul(&self, o: &ComplexAlgebraic) -> ComplexAlgebraic {
94        if self.is_real() && o.is_real() {
95            return ComplexAlgebraic::real(self.re.mul(&o.re));
96        }
97        ComplexAlgebraic {
98            re: self.re.mul(&o.re).sub(&self.im.mul(&o.im)),
99            im: self.re.mul(&o.im).add(&self.im.mul(&o.re)),
100        }
101    }
102
103    /// `self·r` for a rational `r`.
104    pub fn scale(&self, r: &Rational) -> ComplexAlgebraic {
105        let r = Algebraic::from_rational(r.clone());
106        ComplexAlgebraic {
107            re: self.re.mul(&r),
108            im: self.im.mul(&r),
109        }
110    }
111
112    /// `None` at zero.
113    pub fn recip(&self) -> Option<ComplexAlgebraic> {
114        if self.is_real() {
115            return Some(ComplexAlgebraic::real(self.re.recip()?));
116        }
117        let norm = self.re.mul(&self.re).add(&self.im.mul(&self.im));
118        let inv = norm.recip()?;
119        Some(ComplexAlgebraic {
120            re: self.re.mul(&inv),
121            im: self.im.neg().mul(&inv),
122        })
123    }
124
125    pub fn div(&self, o: &ComplexAlgebraic) -> Option<ComplexAlgebraic> {
126        Some(self.mul(&o.recip()?))
127    }
128
129    pub fn pow(&self, e: u32) -> ComplexAlgebraic {
130        let mut acc = ComplexAlgebraic::one();
131        let mut base = self.clone();
132        let mut e = e;
133        while e > 0 {
134            if e & 1 == 1 {
135                acc = acc.mul(&base);
136            }
137            e >>= 1;
138            if e > 0 {
139                base = base.mul(&base);
140            }
141        }
142        acc
143    }
144
145    /// `p(self)` by Horner's rule.
146    pub fn eval(p: &Poly, z: &ComplexAlgebraic) -> ComplexAlgebraic {
147        p.coeffs()
148            .iter()
149            .rev()
150            .fold(ComplexAlgebraic::zero(), |acc, c| {
151                acc.mul(z).add(&ComplexAlgebraic::from_rational(c.clone()))
152            })
153    }
154
155    /// The nearest pair of doubles.
156    pub fn to_f64(&self) -> (f64, f64) {
157        (self.re.to_f64(), self.im.to_f64())
158    }
159}
160
161impl fmt::Display for ComplexAlgebraic {
162    /// `a`, `b·i`, `a + b·i` or `a - b·i`, a unit imaginary part as a bare `i`.
163    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164        if self.is_real() {
165            return write!(f, "{}", self.re);
166        }
167        let magnitude = if self.im.signum() < 0 {
168            self.im.neg()
169        } else {
170            self.im.clone()
171        };
172        let unit = magnitude == Algebraic::from_i64(1);
173        let text = magnitude.to_string();
174        let imaginary = if unit {
175            "i".to_string()
176        } else if text.contains(' ') || text.contains('/') {
177            format!("({text})·i")
178        } else {
179            format!("{text}·i")
180        };
181        match (self.re.is_zero(), self.im.signum() < 0) {
182            (true, false) => write!(f, "{imaginary}"),
183            (true, true) => write!(f, "-{imaginary}"),
184            (false, false) => write!(f, "{} + {imaginary}", self.re),
185            (false, true) => write!(f, "{} - {imaginary}", self.re),
186        }
187    }
188}
189
190/// The real roots of `p` once each, ascending.
191fn distinct_real_roots(p: &Poly) -> Vec<Algebraic> {
192    let mut out: Vec<Algebraic> = Vec::new();
193    for (factor, _) in p.factor_over_q() {
194        for k in 1..=factor.count_real_roots() {
195            if let Some(r) = Algebraic::root_of(&factor, k) {
196                if !out.contains(&r) {
197                    out.push(r);
198                }
199            }
200        }
201    }
202    out.sort();
203    out
204}
205
206/// Floating-point approximations of every root of `p` (degree ≥ 1) by Durand–Kerner iteration.
207fn approximate_roots(p: &Poly) -> Vec<(f64, f64)> {
208    let degree = p.degree().unwrap_or(0);
209    let lead = p.coeff(degree).to_f64();
210    let c: Vec<f64> = (0..=degree).map(|i| p.coeff(i).to_f64() / lead).collect();
211    let eval = |x: (f64, f64)| {
212        let mut acc = (0.0f64, 0.0f64);
213        for k in (0..=degree).rev() {
214            acc = (acc.0 * x.0 - acc.1 * x.1 + c[k], acc.0 * x.1 + acc.1 * x.0);
215        }
216        acc
217    };
218    let seed = (0.4f64, 0.9f64);
219    let mut z: Vec<(f64, f64)> = Vec::with_capacity(degree);
220    let mut w = (1.0f64, 0.0f64);
221    for _ in 0..degree {
222        z.push(w);
223        w = (w.0 * seed.0 - w.1 * seed.1, w.0 * seed.1 + w.1 * seed.0);
224    }
225    for _ in 0..2000 {
226        let mut moved = 0.0f64;
227        for i in 0..degree {
228            let num = eval(z[i]);
229            let mut den = (1.0f64, 0.0f64);
230            for (j, zj) in z.iter().enumerate() {
231                if j != i {
232                    let d = (z[i].0 - zj.0, z[i].1 - zj.1);
233                    den = (den.0 * d.0 - den.1 * d.1, den.0 * d.1 + den.1 * d.0);
234                }
235            }
236            let norm = den.0 * den.0 + den.1 * den.1;
237            if norm == 0.0 {
238                continue;
239            }
240            let step = (
241                (num.0 * den.0 + num.1 * den.1) / norm,
242                (num.1 * den.0 - num.0 * den.1) / norm,
243            );
244            z[i] = (z[i].0 - step.0, z[i].1 - step.1);
245            moved = moved.max(step.0.abs() + step.1.abs());
246        }
247        if moved < 1e-15 {
248            break;
249        }
250    }
251    z
252}
253
254/// The non-real roots of an irreducible `g` with positive imaginary part.
255fn upper_roots(g: &Poly) -> Vec<ComplexAlgebraic> {
256    let degree = g.degree().unwrap_or(0);
257    let pairs = (degree - g.count_real_roots()) / 2;
258    if pairs == 0 {
259        return Vec::new();
260    }
261    let sums = resultant_in_y(
262        g,
263        |x0| g.compose(&Poly::from_coeffs(vec![x0.clone(), Rational::from_i64(-1)])),
264        degree * degree,
265    );
266    let differences = resultant_in_y(
267        g,
268        |x0| g.compose(&Poly::from_coeffs(vec![x0.clone(), Rational::from_i64(1)])),
269        degree * degree,
270    );
271    let half = Algebraic::from_rational(Rational::from_ratio_i64(1, 2).expect("2 ≠ 0"));
272    let real_parts: Vec<Algebraic> = distinct_real_roots(&sums).iter().map(|s| s.mul(&half)).collect();
273    let even: Vec<Rational> = differences.coeffs()[degree..]
274        .iter()
275        .step_by(2)
276        .cloned()
277        .collect();
278    let quarter = Algebraic::from_rational(Rational::from_ratio_i64(-1, 4).expect("4 ≠ 0"));
279    let imaginary_parts: Vec<Algebraic> = distinct_real_roots(&Poly::from_coeffs(even))
280        .into_iter()
281        .filter(|u| u.signum() < 0)
282        .filter_map(|u| u.mul(&quarter).sqrt())
283        .collect();
284    let mut found: Vec<ComplexAlgebraic> = Vec::new();
285    for (x, y) in approximate_roots(g).into_iter().filter(|(_, y)| *y > 0.0) {
286        let mut candidates: Vec<(f64, &Algebraic, &Algebraic)> = Vec::new();
287        for a in &real_parts {
288            for b in &imaginary_parts {
289                let (da, db) = (a.to_f64() - x, b.to_f64() - y);
290                candidates.push((da * da + db * db, a, b));
291            }
292        }
293        candidates.sort_by(|p, q| p.0.total_cmp(&q.0));
294        for (_, a, b) in candidates {
295            let z = ComplexAlgebraic::new(a.clone(), b.clone());
296            if found.contains(&z) {
297                continue;
298            }
299            if ComplexAlgebraic::eval(g, &z).is_zero() {
300                found.push(z);
301                break;
302            }
303        }
304        if found.len() == pairs {
305            break;
306        }
307    }
308    assert_eq!(found.len(), pairs, "every non-real root of {g} is found and checked exactly");
309    found
310}
311
312/// Every complex root of `p` (nonzero), once each: the real roots ascending, then each conjugate
313/// pair by its upper root, upper before lower.
314pub fn complex_roots(p: &Poly) -> Vec<ComplexAlgebraic> {
315    let mut real = Vec::new();
316    let mut pairs = Vec::new();
317    for (g, _) in p.factor_over_q() {
318        for k in 1..=g.count_real_roots() {
319            real.push(Algebraic::root_of(&g, k).expect("a real root of its own polynomial"));
320        }
321        pairs.extend(upper_roots(&g));
322    }
323    real.sort();
324    pairs.sort();
325    let mut out: Vec<ComplexAlgebraic> = real.into_iter().map(ComplexAlgebraic::real).collect();
326    for z in pairs {
327        let lower = z.conj();
328        out.push(z);
329        out.push(lower);
330    }
331    out
332}
333
334#[cfg(test)]
335#[path = "../../logicaffeine_tests/unit/logicaffeine_base/complex_algebraic/tests.rs"]
336mod tests;