Skip to main content

logicaffeine_base/
elimination.rs

1//! Elimination: polynomials in several variables over the rationals, held sparse, and their resultants.
2//!
3//! A resultant eliminates one variable from two polynomials: it vanishes exactly where they share a
4//! root in that variable (or both leading coefficients vanish). The norm of a polynomial whose
5//! coefficients are algebraic numbers is a tower of resultants against their minimal polynomials,
6//! and the projection of a cylindrical decomposition is built from them.
7//!
8//! The resultant is computed by evaluation and interpolation. Specializing every other variable at a
9//! point where neither polynomial's leading coefficient in the eliminated variable vanishes commutes
10//! with taking the resultant, and the resultant's degree in each other variable is bounded by the
11//! degrees of the two polynomials, so the resultant in `v` of polynomials in `n` variables is
12//! interpolated, one variable at a time, from resultants of polynomials in one.
13
14use std::collections::{BTreeMap, BTreeSet};
15use std::fmt;
16
17use crate::numeric::Rational;
18use crate::poly::Poly;
19
20/// A monomial's exponents, variable `i` at index `i`, with no trailing zero.
21type Exponents = Vec<u32>;
22
23fn trimmed(mut e: Exponents) -> Exponents {
24    while e.last() == Some(&0) {
25        e.pop();
26    }
27    e
28}
29
30/// A polynomial in variables numbered from 0, with rational coefficients. No coefficient is zero, so
31/// equal polynomials are structurally equal.
32#[derive(Clone, PartialEq, Eq, Hash, Default)]
33pub struct Polynomial {
34    terms: BTreeMap<Exponents, Rational>,
35}
36
37impl Polynomial {
38    pub fn zero() -> Polynomial {
39        Polynomial::default()
40    }
41
42    pub fn one() -> Polynomial {
43        Polynomial::constant(Rational::one())
44    }
45
46    pub fn constant(c: Rational) -> Polynomial {
47        Polynomial::monomial(c, Vec::new())
48    }
49
50    /// Variable `i`.
51    pub fn var(i: usize) -> Polynomial {
52        let mut e = vec![0; i + 1];
53        e[i] = 1;
54        Polynomial::monomial(Rational::one(), e)
55    }
56
57    /// `c · Π x_i^e_i`.
58    pub fn monomial(c: Rational, exponents: Vec<u32>) -> Polynomial {
59        let mut terms = BTreeMap::new();
60        if !c.is_zero() {
61            terms.insert(trimmed(exponents), c);
62        }
63        Polynomial { terms }
64    }
65
66    /// The univariate `p` in variable `v`.
67    pub fn from_univariate(p: &Poly, v: usize) -> Polynomial {
68        let mut out = Polynomial::zero();
69        for (k, c) in p.coeffs().iter().enumerate() {
70            let mut e = vec![0; v + 1];
71            e[v] = k as u32;
72            out.add_term(e, c.clone());
73        }
74        out
75    }
76
77    pub fn is_zero(&self) -> bool {
78        self.terms.is_empty()
79    }
80
81    /// The value of a polynomial with no variable; `None` for any other.
82    pub fn as_constant(&self) -> Option<Rational> {
83        match self.terms.len() {
84            0 => Some(Rational::zero()),
85            1 => self.terms.get(&Vec::new()).cloned(),
86            _ => None,
87        }
88    }
89
90    /// The terms, exponents with no trailing zero, in increasing order of exponents.
91    pub fn terms(&self) -> impl Iterator<Item = (&[u32], &Rational)> {
92        self.terms.iter().map(|(e, c)| (e.as_slice(), c))
93    }
94
95    fn add_term(&mut self, e: Exponents, c: Rational) {
96        if c.is_zero() {
97            return;
98        }
99        let e = trimmed(e);
100        let sum = match self.terms.get(&e) {
101            Some(old) => old.add(&c),
102            None => c,
103        };
104        if sum.is_zero() {
105            self.terms.remove(&e);
106        } else {
107            self.terms.insert(e, sum);
108        }
109    }
110
111    pub fn add(&self, o: &Polynomial) -> Polynomial {
112        let mut out = self.clone();
113        for (e, c) in &o.terms {
114            out.add_term(e.clone(), c.clone());
115        }
116        out
117    }
118
119    pub fn neg(&self) -> Polynomial {
120        Polynomial {
121            terms: self.terms.iter().map(|(e, c)| (e.clone(), c.negated())).collect(),
122        }
123    }
124
125    pub fn sub(&self, o: &Polynomial) -> Polynomial {
126        self.add(&o.neg())
127    }
128
129    pub fn scale(&self, k: &Rational) -> Polynomial {
130        if k.is_zero() {
131            return Polynomial::zero();
132        }
133        Polynomial {
134            terms: self.terms.iter().map(|(e, c)| (e.clone(), c.mul(k))).collect(),
135        }
136    }
137
138    pub fn mul(&self, o: &Polynomial) -> Polynomial {
139        let mut out = Polynomial::zero();
140        for (e1, c1) in &self.terms {
141            for (e2, c2) in &o.terms {
142                let n = e1.len().max(e2.len());
143                let e: Exponents = (0..n)
144                    .map(|i| e1.get(i).copied().unwrap_or(0) + e2.get(i).copied().unwrap_or(0))
145                    .collect();
146                out.add_term(e, c1.mul(c2));
147            }
148        }
149        out
150    }
151
152    pub fn pow(&self, k: u32) -> Polynomial {
153        let mut acc = Polynomial::one();
154        let mut base = self.clone();
155        let mut e = k;
156        while e > 0 {
157            if e & 1 == 1 {
158                acc = acc.mul(&base);
159            }
160            e >>= 1;
161            if e > 0 {
162                base = base.mul(&base);
163            }
164        }
165        acc
166    }
167
168    /// The degree in variable `v`; zero for the zero polynomial.
169    pub fn degree_in(&self, v: usize) -> u32 {
170        self.terms.keys().map(|e| e.get(v).copied().unwrap_or(0)).max().unwrap_or(0)
171    }
172
173    /// The variables that occur.
174    pub fn variables(&self) -> BTreeSet<usize> {
175        let mut out = BTreeSet::new();
176        for e in self.terms.keys() {
177            for (i, k) in e.iter().enumerate() {
178                if *k > 0 {
179                    out.insert(i);
180                }
181            }
182        }
183        out
184    }
185
186    /// The coefficient of `x_v^k`, a polynomial in the other variables.
187    pub fn coefficient_in(&self, v: usize, k: u32) -> Polynomial {
188        let mut out = Polynomial::zero();
189        for (e, c) in &self.terms {
190            if e.get(v).copied().unwrap_or(0) == k {
191                let mut e = e.clone();
192                if v < e.len() {
193                    e[v] = 0;
194                }
195                out.add_term(e, c.clone());
196            }
197        }
198        out
199    }
200
201    /// The coefficient of the highest power of `x_v`.
202    pub fn leading_in(&self, v: usize) -> Polynomial {
203        self.coefficient_in(v, self.degree_in(v))
204    }
205
206    /// `x_v` set to `x`.
207    pub fn substitute(&self, v: usize, x: &Rational) -> Polynomial {
208        let mut out = Polynomial::zero();
209        for (e, c) in &self.terms {
210            let k = e.get(v).copied().unwrap_or(0);
211            let mut e = e.clone();
212            if v < e.len() {
213                e[v] = 0;
214            }
215            let factor = x.pow(k as i32).expect("a nonnegative power");
216            out.add_term(e, c.mul(&factor));
217        }
218        out
219    }
220
221    /// `x_v` replaced by the polynomial `p`.
222    pub fn compose(&self, v: usize, p: &Polynomial) -> Polynomial {
223        let mut out = Polynomial::zero();
224        for k in 0..=self.degree_in(v) {
225            let c = self.coefficient_in(v, k);
226            if !c.is_zero() {
227                out = out.add(&c.mul(&p.pow(k)));
228            }
229        }
230        out
231    }
232
233    /// `∂/∂x_v`.
234    pub fn derivative(&self, v: usize) -> Polynomial {
235        let mut out = Polynomial::zero();
236        for (e, c) in &self.terms {
237            let k = e.get(v).copied().unwrap_or(0);
238            if k == 0 {
239                continue;
240            }
241            let mut e = e.clone();
242            e[v] = k - 1;
243            out.add_term(e, c.mul(&Rational::from_i64(i64::from(k))));
244        }
245        out
246    }
247
248    /// The polynomial as one in `x_v` alone; `None` when another variable occurs.
249    pub fn to_univariate(&self, v: usize) -> Option<Poly> {
250        if self.variables().iter().any(|w| *w != v) {
251            return None;
252        }
253        let mut c = vec![Rational::zero(); self.degree_in(v) as usize + 1];
254        for (e, k) in &self.terms {
255            c[e.get(v).copied().unwrap_or(0) as usize] = k.clone();
256        }
257        Some(Poly::from_coeffs(c))
258    }
259
260    /// The resultant in `x_v`: `lc^{deg o} · Π o(α)` over the roots `α` in `x_v` of `self`, a
261    /// polynomial in the other variables, by the univariate resultant's convention ([`Poly::resultant`]).
262    /// Zero when either is zero; `self^{deg o}` when `self` does not hold `x_v`.
263    pub fn resultant(&self, o: &Polynomial, v: usize) -> Polynomial {
264        if self.is_zero() || o.is_zero() {
265            return Polynomial::zero();
266        }
267        let (da, db) = (self.degree_in(v), o.degree_in(v));
268        if da == 0 {
269            return self.pow(db);
270        }
271        if db == 0 {
272            return o.pow(da);
273        }
274        let others: BTreeSet<usize> = self.variables().union(&o.variables()).copied().filter(|w| *w != v).collect();
275        let Some(&w) = others.iter().next_back() else {
276            let a = self.to_univariate(v).expect("only x_v occurs");
277            let b = o.to_univariate(v).expect("only x_v occurs");
278            return Polynomial::constant(a.resultant(&b));
279        };
280        let bound = da * o.degree_in(w) + db * self.degree_in(w);
281        let (la, lb) = (self.leading_in(v), o.leading_in(v));
282        let mut nodes: Vec<Rational> = Vec::new();
283        let mut values: Vec<Polynomial> = Vec::new();
284        for x in points() {
285            if nodes.len() > bound as usize {
286                break;
287            }
288            if la.substitute(w, &x).is_zero() || lb.substitute(w, &x).is_zero() {
289                continue;
290            }
291            values.push(self.substitute(w, &x).resultant(&o.substitute(w, &x), v));
292            nodes.push(x);
293        }
294        interpolate(w, &nodes, values)
295    }
296}
297
298/// `0, 1, -1, 2, -2, …`: the points a variable is specialized at.
299fn points() -> impl Iterator<Item = Rational> {
300    (0i64..).map(|i| Rational::from_i64(if i % 2 == 1 { (i + 1) / 2 } else { -(i / 2) }))
301}
302
303/// The polynomial in `x_w` of degree below `nodes.len()` taking `values[i]` at `nodes[i]`, by
304/// Newton's divided differences.
305fn interpolate(w: usize, nodes: &[Rational], mut values: Vec<Polynomial>) -> Polynomial {
306    let n = nodes.len();
307    for j in 1..n {
308        for i in (j..n).rev() {
309            let gap = nodes[i].sub(&nodes[i - j]);
310            let inv = gap.recip().expect("the nodes are distinct");
311            values[i] = values[i].sub(&values[i - 1]).scale(&inv);
312        }
313    }
314    let mut out = values[n - 1].clone();
315    for i in (0..n - 1).rev() {
316        let factor = Polynomial::var(w).sub(&Polynomial::constant(nodes[i].clone()));
317        out = out.mul(&factor).add(&values[i]);
318    }
319    out
320}
321
322impl fmt::Debug for Polynomial {
323    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
324        write!(f, "{self}")
325    }
326}
327
328impl fmt::Display for Polynomial {
329    /// Terms from the highest exponents down, variable `i` written `x{i}`.
330    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
331        if self.is_zero() {
332            return f.write_str("0");
333        }
334        for (n, (e, c)) in self.terms.iter().rev().enumerate() {
335            let monomial: Vec<String> = e
336                .iter()
337                .enumerate()
338                .filter(|(_, k)| **k > 0)
339                .map(|(i, k)| if *k == 1 { format!("x{i}") } else { format!("x{i}^{k}") })
340                .collect();
341            let negative = c.is_negative();
342            let magnitude = if negative { c.negated() } else { c.clone() };
343            let body = match (monomial.is_empty(), magnitude == Rational::one()) {
344                (true, _) => magnitude.to_string(),
345                (false, true) => monomial.join("·"),
346                (false, false) => format!("{magnitude}·{}", monomial.join("·")),
347            };
348            match (n, negative) {
349                (0, true) => write!(f, "-{body}")?,
350                (0, false) => write!(f, "{body}")?,
351                (_, true) => write!(f, " - {body}")?,
352                (_, false) => write!(f, " + {body}")?,
353            }
354        }
355        Ok(())
356    }
357}
358
359#[cfg(test)]
360#[path = "../../logicaffeine_tests/unit/logicaffeine_base/elimination/tests.rs"]
361mod tests;