Skip to main content

logicaffeine_base/
closure.rs

1//! The real closure of the values of several infinitesimals: `√(dt + dx)`, `∛(h + dt)`, every root the
2//! program's arithmetic reaches among the values of more than one infinitesimal, held exactly.
3//!
4//! A value of `dx`, declared much smaller than `dt`, that is no rational function of a root of `dx`
5//! over the values of the larger infinitesimals is an [`AlgValue`]: the recipe that built it, a
6//! squarefree polynomial over those rational functions with the value among its roots, and the value's
7//! expansion in `dx` with exact coefficients ([`crate::expansion`]). The polynomial composes as the
8//! value does — `α + β` is a root of `Res_z(P(z), Q(y − z))`, `α·β` of `Res_z(P(z), z^n·Q(y/z))`, `1/α`
9//! of `P` reversed, `ⁿ√α` of `P(y^n)`, `α^k` of `Res_z(P(z), y − z^k)` — and decides what the expansion
10//! alone cannot. The orders of `P`'s nonzero roots are the slopes of its Newton polygon, so a value
11//! whose expansion vanishes through the largest of them is the root `0`, and a nonzero value's leading
12//! term lies at or below it: every comparison is a bounded scan of exact coefficients.
13//!
14//! **One form for a value.** A value that is a rational function of a root of `dx` is held as that
15//! function ([`Multi::Over`]): its candidate is the Padé approximant of its expansion within the
16//! degrees Gauss's lemma bounds a root of the monic polynomial by, kept only when it is a root of the
17//! polynomial and its difference from the value is the root `0`. A value prints as that function, as a
18//! radical when a power of it is one (`√(dt + dx)`, `2·√(dt + dx)`, `1/√(dt + dx)`), or by its two
19//! leading terms and the order of the rest (`1/(2·√dt) - (1/(8·dt^(3/2)))·dx + O(dx^2)`).
20
21use std::sync::Arc;
22
23use crate::algebraic::Algebraic;
24use crate::expansion::{Expansion, ExpansionError, Source};
25use crate::multi::Multi;
26use crate::numeric::{BigInt, Rational};
27use crate::ordered_field::{OrderedField, PolyOver};
28use crate::puiseux::{expansion_term, remainder, remainder_after, Closed, Puiseux};
29use crate::tower::Scale;
30
31/// How an [`AlgValue`] was built, which is what it is written as.
32#[derive(Clone, Debug)]
33pub(crate) enum Recipe {
34    /// A rational function of a root of the infinitesimal, or a value of the larger ones.
35    Field(Multi),
36    /// A value of the real closure of the infinitesimal alone.
37    Closed(Puiseux),
38    Add(Arc<AlgValue>, Arc<AlgValue>),
39    Neg(Arc<AlgValue>),
40    Mul(Arc<AlgValue>, Arc<AlgValue>),
41    Recip(Arc<AlgValue>),
42    Root(Arc<AlgValue>, u32),
43    Pow(Arc<AlgValue>, u32),
44}
45
46/// A value of the real closure of several infinitesimals at its smallest. See the module
47/// documentation.
48#[derive(Clone, Debug)]
49pub struct AlgValue {
50    pub(crate) scale: Arc<Scale>,
51    pub(crate) recipe: Recipe,
52    /// Squarefree and monic, over the values of `scale` and the larger infinitesimals.
53    pub(crate) poly: PolyOver<Multi>,
54    pub(crate) expansion: Expansion<Multi>,
55}
56
57/// A value of one infinitesimal's real closure read as a value of several: its coefficients.
58#[derive(Debug)]
59struct ClosedSource(Puiseux);
60
61impl Source<Multi> for ClosedSource {
62    fn coefficient(&self, k: i64) -> Result<Multi, ExpansionError> {
63        Ok(Multi::Standard(self.0.coefficient(k)))
64    }
65}
66
67fn gcd_u64(a: u64, b: u64) -> u64 {
68    if b == 0 {
69        a
70    } else {
71        gcd_u64(b, a % b)
72    }
73}
74
75fn lcm_u32(a: u32, b: u32) -> u32 {
76    let g = gcd_u64(u64::from(a), u64::from(b));
77    u32::try_from(u64::from(a) / g * u64::from(b)).expect("a root of an infinitesimal of moderate index")
78}
79
80fn lcm_i64(a: i64, b: i64) -> i64 {
81    a / gcd_u64(a.unsigned_abs(), b.unsigned_abs()) as i64 * b
82}
83
84/// The lower convex hull of points of increasing first coordinate.
85fn lower_hull(points: &[(i64, i64)]) -> Vec<(i64, i64)> {
86    let mut hull: Vec<(i64, i64)> = Vec::new();
87    for &p in points {
88        while hull.len() >= 2 {
89            let (o, a) = (hull[hull.len() - 2], hull[hull.len() - 1]);
90            let cross = (a.0 - o.0) * (p.1 - o.1) - (a.1 - o.1) * (p.0 - o.0);
91            if cross <= 0 {
92                hull.pop();
93            } else {
94                break;
95            }
96        }
97        hull.push(p);
98    }
99    hull
100}
101
102/// The order in `scale` of a nonzero value of `scale` and the larger infinitesimals, in `scale`'s
103/// own units: the exponent of its leading term, and `0` for a value of the larger ones alone.
104fn valuation(a: &Multi, scale: &Arc<Scale>) -> Rational {
105    match a.top() {
106        Some(t) if t.same_infinitesimal(scale) => a.leading().expect("a nonzero value has a leading term").0,
107        _ => Rational::zero(),
108    }
109}
110
111/// How far a scan of an expansion reads for the terms a printed value shows past its first.
112const SHOWN_REACH: i64 = 64;
113
114/// The orders of two values' expansions read for a difference before their difference's polynomial is.
115const EQUALS_ORDERS: i64 = 8;
116
117/// The orders past the ones a Padé approximant `num/den` is built from at which `den·S − num` must vanish
118/// before the candidate is made and checked exactly: an approximant that is no field value is rejected
119/// by its series alone.
120const PADE_CHECK_ORDERS: usize = 4;
121
122impl AlgValue {
123    /// A rational function of a root of `scale`, or a value of the larger infinitesimals, as the one
124    /// root of `y − x`.
125    pub(crate) fn field(x: &Multi, scale: &Arc<Scale>) -> AlgValue {
126        let poly = PolyOver::from(vec![x.neg(), Multi::one()]);
127        let expansion = match x.top() {
128            Some(t) if t.same_infinitesimal(scale) => {
129                let (q, num, den) = x.parts_at(scale);
130                Expansion::fraction(num.c, den.c, q).expect("a value's denominator is nonzero")
131            }
132            _ => Expansion::constant(x.clone()),
133        };
134        AlgValue {
135            scale: scale.clone(),
136            recipe: Recipe::Field(x.clone()),
137            poly,
138            expansion,
139        }
140    }
141
142    /// A value of one infinitesimal's real closure as a value of several: the polynomial it is a root
143    /// of, its coefficients rational functions of a root of `scale`, and its own expansion.
144    pub(crate) fn closed(p: &Puiseux, scale: &Arc<Scale>) -> AlgValue {
145        let (coefficients, q) = p.annihilator();
146        let poly = PolyOver::from(
147            coefficients
148                .into_iter()
149                .map(|h| Multi::of_closed(Closed::of_field(h, q), scale))
150                .collect(),
151        )
152        .monic();
153        let expansion = Expansion::source(Arc::new(ClosedSource(p.clone())), p.order(), p.ramification());
154        AlgValue {
155            scale: scale.clone(),
156            recipe: Recipe::Closed(p.clone()),
157            poly,
158            expansion,
159        }
160    }
161
162    /// `m` as a value of the real closure at `scale`, its smallest infinitesimal or one smaller.
163    pub(crate) fn at(m: &Multi, scale: &Arc<Scale>) -> AlgValue {
164        match m {
165            Multi::Alg(a) if a.scale.same_infinitesimal(scale) => (**a).clone(),
166            Multi::One(Closed::Series(p), s) if s.same_infinitesimal(scale) && p.rational_function().is_none() => {
167                AlgValue::closed(p, scale)
168            }
169            _ => AlgValue::field(m, scale),
170        }
171    }
172
173    /// The largest order, on the expansion's grid, a nonzero root of the polynomial lies at; `None`
174    /// when it has none. Each edge of the Newton polygon of the points `(i, v(a_i))` holds roots of
175    /// the order its slope names.
176    fn reach(&self) -> Option<i64> {
177        let p = self.poly.without_zero_root();
178        if p.degree().unwrap_or(0) == 0 {
179            return None;
180        }
181        let points: Vec<(i64, Rational)> = p
182            .c
183            .iter()
184            .enumerate()
185            .filter(|(_, a)| !a.is_zero())
186            .map(|(i, a)| (i as i64, valuation(a, &self.scale)))
187            .collect();
188        let common = points
189            .iter()
190            .map(|(_, v)| v.denominator().to_i64().expect("a small denominator"))
191            .fold(1i64, lcm_i64);
192        let scaled: Vec<(i64, i64)> = points
193            .iter()
194            .map(|(i, v)| {
195                let w = v.mul(&Rational::from_i64(common));
196                (*i, w.to_bigint().and_then(|b| b.to_i64()).expect("a small order"))
197            })
198            .collect();
199        let hull = lower_hull(&scaled);
200        let largest = hull
201            .windows(2)
202            .map(|e| Rational::from_ratio_i64(e[0].1 - e[1].1, (e[1].0 - e[0].0) * common).expect("a positive run"))
203            .max()?;
204        let on_grid = largest.mul(&Rational::from_i64(i64::from(self.expansion.ramification())));
205        Some(on_grid.ceil().to_i64().expect("a small order"))
206    }
207
208    /// Whether the value is `0`: a root `0` of its polynomial whose expansion vanishes through the
209    /// largest order of the polynomial's nonzero roots.
210    pub(crate) fn is_zero(&self) -> bool {
211        if !self.poly.coeff(0).is_zero() {
212            return false;
213        }
214        match self.reach() {
215            None => true,
216            Some(reach) => self
217                .expansion
218                .leading_through(reach)
219                .expect("exact coefficients")
220                .is_none(),
221        }
222    }
223
224    /// The leading term of a nonzero value: its order on the expansion's grid and its coefficient.
225    pub(crate) fn leading(&self) -> (i64, Multi) {
226        let reach = self.reach().expect("a nonzero value is a nonzero root of its polynomial");
227        self.expansion
228            .leading_through(reach)
229            .expect("exact coefficients")
230            .expect("a nonzero root's leading term lies at or below the largest order of the polynomial's roots")
231    }
232
233    /// The leading order in the infinitesimal's own units.
234    pub(crate) fn order(&self) -> Rational {
235        let (k, _) = self.leading();
236        Rational::from_ratio_i64(k, i64::from(self.expansion.ramification())).expect("q is positive")
237    }
238
239    pub(crate) fn sign(&self) -> i32 {
240        OrderedField::signum(&self.leading().1)
241    }
242
243    fn of(&self, recipe: Recipe, poly: PolyOver<Multi>, expansion: Expansion<Multi>) -> AlgValue {
244        AlgValue {
245            scale: self.scale.clone(),
246            recipe,
247            poly: poly.squarefree(),
248            expansion,
249        }
250    }
251
252    pub(crate) fn add(a: &Arc<AlgValue>, b: &Arc<AlgValue>) -> Multi {
253        let poly = PolyOver::sum_poly(&a.poly, &b.poly);
254        a.of(Recipe::Add(a.clone(), b.clone()), poly, a.expansion.add(&b.expansion)).canonical()
255    }
256
257    pub(crate) fn neg(a: &Arc<AlgValue>) -> Multi {
258        a.of(Recipe::Neg(a.clone()), a.poly.neg_var(), a.expansion.neg()).canonical()
259    }
260
261    /// The product of two nonzero values.
262    pub(crate) fn mul(a: &Arc<AlgValue>, b: &Arc<AlgValue>) -> Multi {
263        let poly = PolyOver::product_poly(&a.poly.without_zero_root(), &b.poly.without_zero_root());
264        a.of(Recipe::Mul(a.clone(), b.clone()), poly, a.expansion.mul(&b.expansion)).canonical()
265    }
266
267    /// The reciprocal of a nonzero value.
268    pub(crate) fn recip(a: &Arc<AlgValue>) -> Multi {
269        let (k, c) = a.leading();
270        a.of(Recipe::Recip(a.clone()), a.poly.reversed(), a.expansion.recip(k, c)).canonical()
271    }
272
273    /// The real `n`-th root of a nonzero value, the positive one for an even `n`.
274    pub(crate) fn root(a: &Arc<AlgValue>, n: u32) -> Result<Multi, String> {
275        let (k, c) = a.leading();
276        let expansion = a.expansion.root(n, k, c).map_err(|e| e.to_string())?;
277        Ok(a.of(Recipe::Root(a.clone(), n), a.poly.spread(n as usize), expansion).canonical())
278    }
279
280    /// `a^k` for `k ≥ 1`: the polynomial of the `k`-th powers of its roots, and the product of `k`
281    /// expansions by repeated squaring.
282    pub(crate) fn pow(a: &Arc<AlgValue>, k: u32) -> Multi {
283        let poly = PolyOver::power_poly(&a.poly, k);
284        let mut base = a.expansion.clone();
285        let mut acc: Option<Expansion<Multi>> = None;
286        let mut e = k;
287        while e > 0 {
288            if e & 1 == 1 {
289                acc = Some(match acc {
290                    None => base.clone(),
291                    Some(x) => x.mul(&base),
292                });
293            }
294            e >>= 1;
295            if e > 0 {
296                base = base.mul(&base);
297            }
298        }
299        a.of(Recipe::Pow(a.clone(), k), poly, acc.expect("k ≥ 1")).canonical()
300    }
301
302    /// The value in its one form: `0`, the rational function it is, or itself.
303    fn canonical(self) -> Multi {
304        if self.is_zero() {
305            return <Multi as OrderedField>::zero();
306        }
307        if self.poly.degree() == Some(1) {
308            return self.poly.coeff(0).neg().mul(&self.poly.coeff(1).recip().expect("a leading coefficient is nonzero"));
309        }
310        if let Some(x) = self.field_value() {
311            return x;
312        }
313        Multi::Alg(Arc::new(self))
314    }
315
316    /// The rational function of a root of the infinitesimal the value is, when it is one. See the
317    /// module documentation.
318    fn field_value(&self) -> Option<Multi> {
319        let scale = &self.scale;
320        let monic = self.poly.monic();
321        let parts: Vec<(u32, PolyOver<Multi>, PolyOver<Multi>)> = monic
322            .c
323            .iter()
324            .map(|a| match a {
325                Multi::Alg(_) => None,
326                Multi::One(c, s) if s.same_infinitesimal(scale) && c.field().is_none() => None,
327                _ => Some(a.parts_at(scale)),
328            })
329            .collect::<Option<_>>()?;
330        let qe = self.expansion.ramification();
331        let q = parts.iter().fold(qe, |l, (qa, _, _)| lcm_u32(l, *qa));
332        let degrees: Vec<(usize, usize)> = parts
333            .iter()
334            .map(|(qa, n, d)| {
335                let m = (q / qa) as usize;
336                (n.degree().unwrap_or(0) * m, d.degree().unwrap_or(0) * m)
337            })
338            .collect();
339        let lcm_bound: usize = degrees.iter().map(|(_, d)| d).sum();
340        let (n0, d0) = degrees[0];
341        let num_bound = (lcm_bound + n0).checked_sub(d0)?;
342        let refined = self.expansion.refined(q / qe);
343        let (k, _) = self.leading();
344        let v = k * i64::from(q / qe);
345        let (l, m) = if v >= 0 {
346            (num_bound.checked_sub(usize::try_from(v).ok()?)?, lcm_bound)
347        } else {
348            (num_bound, lcm_bound.checked_sub(usize::try_from(-v).ok()?)?)
349        };
350        let series: Vec<Multi> = (0..=(l + m) as i64)
351            .map(|i| refined.coefficient(v + i))
352            .collect::<Result<_, _>>()
353            .ok()?;
354        let (num, den) = PolyOver::pade(&series, l, m)?;
355        'past: for k in (l + m + 1)..=(l + m + PADE_CHECK_ORDERS) {
356            let mut past = OrderedField::neg(&num.coeff(k));
357            for (j, d) in den.c.iter().enumerate().take(k + 1) {
358                let Ok(term) = refined.coefficient(v + (k - j) as i64) else {
359                    break 'past;
360                };
361                past = OrderedField::add(&past, &OrderedField::mul(d, &term));
362            }
363            if !OrderedField::is_zero(&past) {
364                return None;
365            }
366        }
367        let shift = |p: &PolyOver<Multi>, by: usize| {
368            let mut c = vec![<Multi as OrderedField>::zero(); by];
369            c.extend(p.c.iter().cloned());
370            PolyOver::from(c)
371        };
372        let (num, den) = if v >= 0 {
373            (shift(&num, v as usize), den)
374        } else {
375            (num, shift(&den, (-v) as usize))
376        };
377        let candidate = Multi::make(scale, q, num, den);
378        if !OrderedField::is_zero(&monic.eval(&candidate)) {
379            return None;
380        }
381        let at_candidate = AlgValue::field(&candidate, scale);
382        let difference = AlgValue {
383            scale: scale.clone(),
384            recipe: Recipe::Field(candidate.clone()),
385            poly: monic.shift(&candidate),
386            expansion: self.expansion.sub(&at_candidate.expansion),
387        };
388        difference.is_zero().then_some(candidate)
389    }
390
391    /// Whether the two are built the same way from the same values, and so are one value.
392    fn same_recipe(a: &Arc<AlgValue>, b: &Arc<AlgValue>) -> bool {
393        if Arc::ptr_eq(a, b) {
394            return true;
395        }
396        if !a.scale.same_infinitesimal(&b.scale) {
397            return false;
398        }
399        let same = AlgValue::same_recipe;
400        match (&a.recipe, &b.recipe) {
401            (Recipe::Field(x), Recipe::Field(y)) => x == y,
402            (Recipe::Closed(x), Recipe::Closed(y)) => Closed::Series(x.clone()) == Closed::Series(y.clone()),
403            (Recipe::Add(a1, a2), Recipe::Add(b1, b2)) | (Recipe::Mul(a1, a2), Recipe::Mul(b1, b2)) => {
404                same(a1, b1) && same(a2, b2)
405            }
406            (Recipe::Neg(x), Recipe::Neg(y)) | (Recipe::Recip(x), Recipe::Recip(y)) => same(x, y),
407            (Recipe::Root(x, n), Recipe::Root(y, m)) | (Recipe::Pow(x, n), Recipe::Pow(y, m)) => n == m && same(x, y),
408            _ => false,
409        }
410    }
411
412    /// Whether the two are one value: their difference is `0`. Two values built the same way are; two
413    /// values that differ differ in their expansions, so a difference among the first
414    /// [`EQUALS_ORDERS`] orders both read answers at once; only values that agree that far are told
415    /// apart by their difference's polynomial.
416    pub(crate) fn equals(a: &Arc<AlgValue>, b: &Arc<AlgValue>) -> bool {
417        if !a.scale.same_infinitesimal(&b.scale) {
418            return false;
419        }
420        if AlgValue::same_recipe(a, b) {
421            return true;
422        }
423        let difference = a.expansion.sub(&b.expansion);
424        let through = difference.floor() + EQUALS_ORDERS * i64::from(difference.ramification());
425        if let Ok(Some(_)) = difference.leading_through(through) {
426            return false;
427        }
428        OrderedField::is_zero(&OrderedField::sub(&Multi::Alg(a.clone()), &Multi::Alg(b.clone())))
429    }
430
431    // ── printing ──────────────────────────────────────────────────────────────────────────────
432
433    /// A radical when a power of the value is a rational function of a root of its infinitesimal,
434    /// otherwise its two leading terms and the order of the rest. A value of degree two — its
435    /// polynomial, a zero root aside, `z² + p·z + q`, its minimal one since the value is no field
436    /// value — is a radical exactly when `p = 0`: were `y² ∈ K` with `p ≠ 0`, the conjugate
437    /// `y₂ = −p − y` would have `y₂² ∈ K` too, and `y = (−p − (y² − y₂²)/p)/2` would lie in `K`.
438    pub(crate) fn render(a: &Arc<AlgValue>) -> String {
439        if let Some(text) = AlgValue::radical(a) {
440            return text;
441        }
442        a.leading_terms()
443    }
444
445    /// `F·ⁿ√κ` for the least `n` whose power of the value is a rational function `κ` of a root of the
446    /// infinitesimal, an integer factor of `κ` that is an `n`-th power taken out into `F`; a value whose
447    /// power has a constant numerator prints as one over the radical of the reciprocal.
448    fn radical(a: &Arc<AlgValue>) -> Option<String> {
449        let degree = a.poly.degree().unwrap_or(0) as u32;
450        let own = a.poly.without_zero_root();
451        if own.degree() == Some(2) && !OrderedField::is_zero(&own.coeff(1)) {
452            return None;
453        }
454        let negative = a.sign() < 0;
455        for n in 2..=degree.max(2) {
456            let kappa = AlgValue::pow(a, n);
457            if matches!(kappa, Multi::Alg(_)) {
458                continue;
459            }
460            let (num, den) = kappa.fraction_parts();
461            let inverted = num.top().is_none() && den.top().is_some();
462            let kappa = if inverted { kappa.recip()? } else { kappa };
463            let (num, den) = kappa.fraction_parts();
464            let (cn, num) = integer_content(&num);
465            let (cd, den) = integer_content(&den);
466            let constant = Rational::new(cn, cd).expect("a content is positive");
467            let root = Algebraic::from_rational(constant.clone())
468                .nth_root(n)
469                .and_then(|r| r.to_rational());
470            let (outside, inside) = match root {
471                Some(r) => (r, num.mul(&den.recip().expect("a denominator is nonzero"))),
472                None => (
473                    Rational::one(),
474                    num.mul(&den.recip().expect("a denominator is nonzero"))
475                        .mul(&Multi::Standard(Algebraic::from_rational(constant))),
476                ),
477            };
478            let outside = if n % 2 == 0 && negative { outside.negated() } else { outside };
479            let radicand = inside.render();
480            let radicand = if radicand.contains([' ', '/', '·', '^']) { format!("({radicand})") } else { radicand };
481            let sign = radical_sign(n);
482            let factor = outside.to_string();
483            let (minus, factor) = match factor.strip_prefix('-') {
484                Some(rest) => ("-", rest.to_string()),
485                None => ("", factor),
486            };
487            let factor = if factor.contains('/') { format!("({factor})") } else { factor };
488            let root = if factor == "1" { format!("{sign}{radicand}") } else { format!("{factor}·{sign}{radicand}") };
489            return Some(if inverted {
490                let root = if root.contains('·') { format!("({root})") } else { root };
491                format!("{minus}1/{root}")
492            } else {
493                format!("{minus}{root}")
494            });
495        }
496        None
497    }
498
499    /// The two leading terms and, from the order after the last one shown, what is left.
500    fn leading_terms(&self) -> String {
501        let name = &self.scale.name;
502        let q = i64::from(self.expansion.ramification());
503        let (k0, c0) = self.leading();
504        let next_after = |from: i64| -> Option<(i64, Multi)> {
505            for k in from + 1..=from + SHOWN_REACH * q {
506                let c = self.expansion.coefficient(k).expect("exact coefficients");
507                if !OrderedField::is_zero(&c) {
508                    return Some((k, c));
509                }
510            }
511            None
512        };
513        let exponent = |k: i64| Rational::from_ratio_i64(k, q).expect("q is positive");
514        let mut terms = vec![(k0, c0)];
515        if let Some(second) = next_after(k0) {
516            terms.push(second);
517        }
518        let mut out = String::new();
519        for (i, (k, c)) in terms.iter().enumerate() {
520            let t = expansion_term(name, &c.render(), &exponent(*k));
521            match (i, t.strip_prefix('-')) {
522                (0, _) => out.push_str(&t),
523                (_, Some(rest)) => {
524                    out.push_str(" - ");
525                    out.push_str(rest);
526                }
527                (_, None) => {
528                    out.push_str(" + ");
529                    out.push_str(&t);
530                }
531            }
532        }
533        let shown: Vec<Rational> = terms.iter().map(|(k, _)| exponent(*k)).collect();
534        let last = terms.last().expect("a term is shown").0;
535        let next = next_after(last).map(|(k, _)| exponent(k)).unwrap_or_else(|| {
536            exponent(last + SHOWN_REACH * q)
537        });
538        format!("{out} + {}", remainder(name, &remainder_after(&shown, &next)))
539    }
540}
541
542/// `√`, `∛`, `∜`, or the index written small before `√`.
543fn radical_sign(n: u32) -> String {
544    match n {
545        2 => "√".to_string(),
546        3 => "∛".to_string(),
547        4 => "∜".to_string(),
548        n => {
549            let digits: String = n
550                .to_string()
551                .chars()
552                .map(|d| ['⁰', '¹', '²', '³', '⁴', '⁵', '⁶', '⁷', '⁸', '⁹'][d.to_digit(10).expect("a digit") as usize])
553                .collect();
554            format!("{digits}√")
555        }
556    }
557}
558
559/// A polynomial in the infinitesimals as its positive integer content and the rest: the gcd of its
560/// standard integer coefficients, `1` when any is not one.
561fn integer_content(p: &Multi) -> (BigInt, Multi) {
562    let mut leaves: Vec<Rational> = Vec::new();
563    if !collect_leaves(p, &mut leaves) {
564        return (BigInt::from_i64(1), p.clone());
565    }
566    let g = leaves
567        .iter()
568        .filter(|r| !r.is_zero())
569        .fold(BigInt::from_i64(0), |g, r| g.gcd(r.numerator()));
570    if g.is_zero() || g == BigInt::from_i64(1) {
571        return (BigInt::from_i64(1), p.clone());
572    }
573    let inv = Multi::Standard(Algebraic::from_rational(Rational::new(BigInt::from_i64(1), g.clone()).expect("g ≠ 0")));
574    (g, p.mul(&inv))
575}
576
577/// The standard coefficients of a polynomial in the infinitesimals, when every one is an integer.
578fn collect_leaves(p: &Multi, out: &mut Vec<Rational>) -> bool {
579    match p {
580        Multi::Standard(a) => match a.to_rational().filter(Rational::is_integer) {
581            Some(r) => {
582                out.push(r);
583                true
584            }
585            None => false,
586        },
587        Multi::One(c, _) => match c.field() {
588            Some((h, _)) => {
589                let (num, den) = h.parts();
590                if *den != PolyOver::one() {
591                    return false;
592                }
593                num.c.iter().all(|a| match a.to_rational().filter(Rational::is_integer) {
594                    Some(r) => {
595                        out.push(r);
596                        true
597                    }
598                    None => false,
599                })
600            }
601            None => false,
602        },
603        Multi::Over { num, den, .. } => {
604            *den == PolyOver::one() && num.c.iter().all(|c| collect_leaves(c, out))
605        }
606        Multi::Alg(_) => false,
607    }
608}
609
610#[cfg(test)]
611#[path = "../../logicaffeine_tests/unit/logicaffeine_base/closure/tests.rs"]
612mod tests;