symplex/api/expr_algebraic_ext.rs
1//! Algebraic-number and polynomial-algebra conveniences on [`Ex`]:
2//! minimal polynomials, multivariate gcd/lcm, Gröbner bases, real roots
3//! as `RootOf`, factoring modulo a prime, symbolic resultants. (0.9)
4//!
5//! Everything here is a thin bridge from `Ex` to the crate's exact
6//! polynomial engines (`poly::algebraic`, `poly::multipoly`,
7//! `poly::groebner`, `poly::factor_zassenhaus`, `poly::sturm`) and back.
8//! Methods that return `Option` yield `None` when the input is not of the
9//! required shape (Pattern 4: a query that cannot be answered); methods
10//! that validate caller-supplied structure (variable lists, moduli) return
11//! `Result` (Pattern 5).
12
13use num_bigint::BigInt;
14use num_rational::Ratio;
15use num_traits::{Signed, Zero};
16
17use crate::api::expr::{Ex, Expr, ExprType, Numeric};
18use crate::base::arena::Arena;
19use crate::base::errors::SymplexError;
20use crate::base::node::{ExprId, ExprNode};
21use crate::poly::Poly;
22use crate::poly::groebner::{groebner_basis, groebner_basis_lex};
23use crate::poly::multipoly::{GrevLex, Lex, MultiPoly};
24use crate::poly::polybridge::{expr_to_multipoly, expr_to_poly, multipoly_to_expr, poly_to_expr};
25use crate::poly::sturm::SturmChain;
26
27pub use crate::poly::multipoly::MonomialOrder;
28
29// ═══════════════════════════════════════════════════════════════════════════
30// Arena-level helpers
31// ═══════════════════════════════════════════════════════════════════════════
32
33fn invalid(operation: &'static str, reason: impl Into<String>) -> SymplexError {
34 SymplexError::invalid_argument(operation, reason)
35}
36
37/// The polynomial with integer coefficients, no common integer factor and
38/// positive leading coefficient that is a rational multiple of `p`
39/// (SymPy's normalisation of `minimal_polynomial`).
40fn integer_primitive(p: &Poly) -> Poly {
41 let prim = p.primitive_part();
42 match prim.leading_coeff() {
43 Some(lc) if lc.is_negative() => -&prim,
44 _ => prim,
45 }
46}
47
48/// Union of the free symbols of `exprs`, in the arena's canonical order.
49fn shared_generators(arena: &Arena, exprs: &[ExprId]) -> Vec<ExprId> {
50 let mut gens: Vec<ExprId> = Vec::new();
51 for &e in exprs {
52 for s in crate::base::walk::free_symbols(arena, e) {
53 if !gens.contains(&s) {
54 gens.push(s);
55 }
56 }
57 }
58 gens.sort_by(|a, b| {
59 arena
60 .sort_key(*a)
61 .cmp(arena.sort_key(*b))
62 .then_with(|| a.0.cmp(&b.0))
63 });
64 gens
65}
66
67/// Validate a variable list for the Gröbner API: non-empty, distinct
68/// symbols of the same context as `probe`.
69fn validate_vars(
70 probe: &Ex,
71 vars: &[Ex],
72 operation: &'static str,
73) -> Result<Vec<ExprId>, SymplexError> {
74 if vars.is_empty() {
75 return Err(invalid(operation, "at least one variable is required"));
76 }
77 let mut ids: Vec<ExprId> = Vec::with_capacity(vars.len());
78 for v in vars {
79 let id = probe.checked_id(v);
80 if v.expr_type() != ExprType::Symbol {
81 return Err(invalid(
82 operation,
83 format!("variable `{v}` is not a symbol"),
84 ));
85 }
86 if ids.contains(&id) {
87 return Err(invalid(operation, format!("duplicate variable `{v}`")));
88 }
89 ids.push(id);
90 }
91 Ok(ids)
92}
93
94/// Convert every expression to a `MultiPoly` over `vars`, naming the first
95/// one that is not a polynomial with rational coefficients.
96fn to_multipolys(
97 arena: &Arena,
98 exprs: &[ExprId],
99 vars: &[ExprId],
100 operation: &'static str,
101) -> Result<Vec<MultiPoly<GrevLex>>, SymplexError> {
102 exprs
103 .iter()
104 .map(|&e| {
105 expr_to_multipoly(arena, e, vars).ok_or_else(|| {
106 invalid(
107 operation,
108 format!(
109 "`{}` is not a polynomial in the given variables with rational coefficients",
110 arena.display(e)
111 ),
112 )
113 })
114 })
115 .collect()
116}
117
118/// Reduced Gröbner basis of `polys` under `order`, returned in grevlex
119/// storage (the order only affects which basis is computed).
120fn groebner_in_order(
121 polys: &[MultiPoly<GrevLex>],
122 order: MonomialOrder,
123) -> Vec<MultiPoly<GrevLex>> {
124 match order {
125 MonomialOrder::GrevLex => groebner_basis(polys),
126 MonomialOrder::Lex => groebner_basis_lex(polys)
127 .iter()
128 .map(MultiPoly::convert_order)
129 .collect(),
130 }
131}
132
133/// Remainder of `f` on division by `basis` under `order`.
134fn reduce_in_order(
135 f: &MultiPoly<GrevLex>,
136 basis: &[MultiPoly<GrevLex>],
137 order: MonomialOrder,
138) -> MultiPoly<GrevLex> {
139 match order {
140 MonomialOrder::GrevLex => {
141 let refs: Vec<&MultiPoly<GrevLex>> = basis.iter().collect();
142 f.reduce(&refs)
143 }
144 MonomialOrder::Lex => {
145 let lex_basis: Vec<MultiPoly<Lex>> =
146 basis.iter().map(MultiPoly::convert_order).collect();
147 let refs: Vec<&MultiPoly<Lex>> = lex_basis.iter().collect();
148 f.convert_order::<Lex>().reduce(&refs).convert_order()
149 }
150 }
151}
152
153/// One distinct real root of a polynomial, before it is interned.
154enum RealRoot {
155 /// A rational root (of a linear factor).
156 Rational(Ratio<BigInt>),
157 /// `RootOf(g, k)`: the `k`-th root of the irreducible factor `g` in the
158 /// (re, im) order the `RootOf` evaluator uses.
159 RootOf { factor: usize, index: usize },
160}
161
162/// The distinct real roots of `f` (a non-constant polynomial with rational
163/// coefficients), ascending, together with the irreducible factors over ℤ
164/// that the `RootOf` entries refer to. Rational roots are exact; every
165/// other root is named `RootOf(g, k)` with `k` from
166/// [`real_root_index`](crate::poly::roots::real_root_index), which
167/// verifies that the `k`-th computed root of `g` lies in the root's Sturm
168/// isolating interval and that the index is stable.
169///
170/// Returns `None` when the factorisation over ℤ is not certified complete
171/// (a `RootOf` must name an *irreducible* polynomial), when a root cannot
172/// be assigned to exactly one factor, or when its `RootOf` index cannot be
173/// verified. These are internal limits, not a statement about `f`; the
174/// public wrapper folds them into its `None` alongside "not a polynomial"
175/// because its return type cannot distinguish the two in a patch release.
176fn real_roots_of(f: &Poly) -> Option<(Vec<Poly>, Vec<RealRoot>)> {
177 let (_content, parts, complete) = crate::poly::factor_zassenhaus::factor_zassenhaus_checked(f);
178 if !complete {
179 return None;
180 }
181 let mut factors: Vec<Poly> = Vec::with_capacity(parts.len());
182 let mut chains: Vec<SturmChain> = Vec::with_capacity(parts.len());
183 let mut square_free = Poly::from_int(1);
184 for (g, _mult) in &parts {
185 if g.degree()? == 0 {
186 continue;
187 }
188 square_free = &square_free * g;
189 chains.push(SturmChain::new(g));
190 factors.push(g.clone());
191 }
192 if square_free.degree().unwrap_or(0) == 0 {
193 return None;
194 }
195
196 // Isolating intervals of the product of the distinct factors are
197 // pairwise disjoint and sorted, so walking them in order visits every
198 // real root ascending; each one belongs to exactly one factor.
199 // Each interval is either the point `[r, r]` of an exact hit or a
200 // half-open `(lo, hi]` Sturm cell, so `count_roots_in` (also `(a, b]`)
201 // is the right membership test for the latter.
202 let intervals = SturmChain::new(&square_free).isolate_all_real_roots();
203 let mut out: Vec<RealRoot> = Vec::with_capacity(intervals.len());
204 for iv in intervals {
205 let owner = factors.iter().zip(&chains).position(|(g, chain)| {
206 if iv.is_point() {
207 g.eval(&iv.lower).is_zero()
208 } else {
209 chain.count_roots_in(&iv.lower, &iv.upper) == 1
210 }
211 })?;
212 let g = &factors[owner];
213 if g.degree() == Some(1) {
214 out.push(RealRoot::Rational(-(g.coeff(0) / g.coeff(1))));
215 } else {
216 // `real_root_index` treats the pair as closed `[lo, hi]`, a
217 // superset of `(lo, hi]`; the extra endpoint is not a root of
218 // `g` (an exact hit is reported as a point instead).
219 let index = crate::poly::roots::real_root_index(g, &iv.lower, &iv.upper)?;
220 out.push(RealRoot::RootOf {
221 factor: owner,
222 index,
223 });
224 }
225 }
226 Some((factors, out))
227}
228
229/// Determinant of the Sylvester matrix of two coefficient lists (highest
230/// degree first, both non-empty with non-zero leading coefficient), i.e.
231/// `res(f, g)`. `f` of degree `m`, `g` of degree `n`: the matrix is
232/// `(m + n) × (m + n)` with `n` shifted copies of `f` above `m` shifted
233/// copies of `g`.
234fn sylvester_resultant(ctx: &crate::api::context::Context, f: &[Ex], g: &[Ex]) -> Option<Ex> {
235 let m = f.len().checked_sub(1)?;
236 let n = g.len().checked_sub(1)?;
237 if m == 0 && n == 0 {
238 return Some(ctx.one());
239 }
240 if m == 0 {
241 return Some(f[0].powi(n as i64));
242 }
243 if n == 0 {
244 return Some(g[0].powi(m as i64));
245 }
246 let size = m + n;
247 let zero = ctx.zero();
248 let mut rows: Vec<Vec<Ex>> = Vec::with_capacity(size);
249 for i in 0..n {
250 let mut row = vec![zero.clone(); size];
251 for (j, c) in f.iter().enumerate() {
252 row[i + j] = c.clone();
253 }
254 rows.push(row);
255 }
256 for i in 0..m {
257 let mut row = vec![zero.clone(); size];
258 for (j, c) in g.iter().enumerate() {
259 row[i + j] = c.clone();
260 }
261 rows.push(row);
262 }
263 // Invariant: `rows` is a non-empty square `size × size` matrix, so
264 // `Matrix::new` and `det` cannot fail. Their `Err` is an internal
265 // error, not "not a polynomial"; the `Option` return of the public
266 // callers cannot express that in a patch release, so it is folded into
267 // `None` here (and asserted in debug builds).
268 let matrix = crate::domains::matrix::Matrix::new(rows);
269 debug_assert!(matrix.is_ok(), "Sylvester matrix is square by construction");
270 let det = matrix.ok()?.det();
271 debug_assert!(det.is_ok(), "determinant of a square matrix over Ex");
272 Some(det.ok()?.expand())
273}
274
275// ═══════════════════════════════════════════════════════════════════════════
276// impl Expr<Numeric> — algebraic numbers and polynomial algebra
277// ═══════════════════════════════════════════════════════════════════════════
278
279impl Expr<Numeric> {
280 // ── Minimal polynomial ─────────────────────────────────────────
281
282 /// Minimal polynomial over ℚ of an algebraic-number expression, as a
283 /// polynomial in `var` (SymPy `minimal_polynomial`).
284 ///
285 /// `self` must be a constant built from rational numbers, radicals
286 /// (`n^{p/q}`), `i`, `φ`, sums, products, negations, and integer or
287 /// rational powers of such numbers (`(1 + √2)⁻¹`, `√(3 + 2√2)`; a
288 /// fractional power needs a positive real base). The result has
289 /// integer coefficients with no common factor and a positive leading
290 /// coefficient, so `3/4` gives `4·var − 3` and `√2 + √3` gives
291 /// `var⁴ − 10·var² + 1`. Returns `None` for input that is not
292 /// recognised as algebraic — `π`, `e`, free symbols, transcendental
293 /// functions.
294 ///
295 /// The result is verified, never guessed: the irreducible factor of
296 /// each intermediate resultant is chosen by evaluating the number to
297 /// 320 bits and accepting the unique factor whose residual is
298 /// negligible, and the factorisation itself must be certified complete.
299 /// If either check fails the method returns `None` rather than a
300 /// polynomial that might be reducible or vanish at a different
301 /// conjugate.
302 ///
303 /// # Examples
304 ///
305 /// ```
306 /// use symplex::prelude::*;
307 ///
308 /// let ctx = Context::new();
309 /// let x = ctx.symbol("x");
310 /// let a = ctx.int(2).sqrt() + ctx.int(3).sqrt();
311 /// let m = a.minimal_polynomial(&x).unwrap();
312 /// assert_eq!(m, &x.powi(4) - &x.powi(2) * 10 + 1);
313 ///
314 /// let cbrt2 = ctx.int(2).pow(&ctx.rational(1, 3));
315 /// assert_eq!(cbrt2.minimal_polynomial(&x).unwrap(), &x.powi(3) - 2);
316 ///
317 /// assert!(ctx.pi().minimal_polynomial(&x).is_none());
318 /// ```
319 #[must_use]
320 pub fn minimal_polynomial(&self, var: &Ex) -> Option<Ex> {
321 use crate::poly::algebraic::{AlgExpr, minimal_polynomial_of};
322 let var_id = self.checked_id(var);
323 // Detach the expression under a read lock; the computation itself
324 // (resultants, factoring, 320-bit evaluation) runs unlocked.
325 let alg = {
326 let inner = self.inner.read();
327 AlgExpr::from_arena(&inner.arena, self.raw_id())
328 };
329 let mp = alg.and_then(|a| minimal_polynomial_of(&a)).or_else(|| {
330 // A product or power the structural recursion does not
331 // recognise may become a plain sum once expanded; try that
332 // form once before giving up.
333 let expanded = self.expand();
334 if expanded == *self {
335 return None;
336 }
337 let alg = {
338 let inner = self.inner.read();
339 AlgExpr::from_arena(&inner.arena, expanded.raw_id())
340 }?;
341 minimal_polynomial_of(&alg)
342 })?;
343 let id = {
344 let mut inner = self.inner.write();
345 poly_to_expr(&mut inner.arena, &integer_primitive(&mp), var_id)
346 };
347 Some(self.wrap(id))
348 }
349
350 // ── Multivariate gcd / lcm ─────────────────────────────────────
351
352 /// Polynomial greatest common divisor of `self` and `other` over ℚ in
353 /// all of their free symbols at once (SymPy `gcd(f, g)`).
354 ///
355 /// Both inputs must be polynomials with rational coefficients; anything
356 /// else (`sin(x)`, `1/x`, `π`) gives `None`. The result is normalised
357 /// like [`MultiPoly::gcd`]: integer coefficients, positive leading
358 /// coefficient in graded reverse lexicographic order, and integer
359 /// content equal to the gcd of the inputs' integer contents once their
360 /// denominators are cleared — for polynomials over ℤ this is exactly
361 /// the gcd over ℤ (`gcd(2x, 4x) = 2x`). `gcd(0, 0) = 0`; two rational
362 /// constants give their integer gcd.
363 ///
364 /// # Examples
365 ///
366 /// ```
367 /// use symplex::prelude::*;
368 ///
369 /// let ctx = Context::new();
370 /// let (x, y) = (ctx.symbol("x"), ctx.symbol("y"));
371 /// let f = &x.powi(2) - &y.powi(2);
372 /// let g = &x - &y;
373 /// assert_eq!(f.gcd_all(&g).unwrap(), g);
374 /// assert_eq!((&x * 2).gcd_all(&(&x * 4)).unwrap(), &x * 2);
375 /// assert!(x.sin().gcd_all(&x).is_none());
376 /// ```
377 #[must_use]
378 pub fn gcd_all(&self, other: &Ex) -> Option<Ex> {
379 self.multipoly_binary(other, MultiPoly::gcd)
380 }
381
382 /// Polynomial least common multiple of `self` and `other` over ℚ in
383 /// all of their free symbols (SymPy `lcm(f, g)`).
384 ///
385 /// Same preconditions and normalisation as [`gcd_all`](Self::gcd_all):
386 /// `lcm = a·b / gcd(a, b)` computed on the integer-normalised inputs,
387 /// so `lcm(2x, 4x) = 4x`; zero if either input is zero.
388 ///
389 /// # Examples
390 ///
391 /// ```
392 /// use symplex::prelude::*;
393 ///
394 /// let ctx = Context::new();
395 /// let (x, y) = (ctx.symbol("x"), ctx.symbol("y"));
396 /// let f = &x.powi(2) - &y.powi(2);
397 /// let g = &x - &y;
398 /// assert_eq!(f.lcm_all(&g).unwrap(), f);
399 /// assert_eq!((&x * &y).lcm_all(&y.powi(2)).unwrap(), &x * &y.powi(2));
400 /// ```
401 #[must_use]
402 pub fn lcm_all(&self, other: &Ex) -> Option<Ex> {
403 self.multipoly_binary(other, MultiPoly::lcm)
404 }
405
406 /// Shared driver of `gcd_all` / `lcm_all`.
407 fn multipoly_binary(
408 &self,
409 other: &Ex,
410 op: fn(&MultiPoly<GrevLex>, &MultiPoly<GrevLex>) -> MultiPoly<GrevLex>,
411 ) -> Option<Ex> {
412 let other_id = self.checked_id(other);
413 // Convert under a read lock, compute unlocked, intern under a write
414 // lock.
415 let (gens, a, b) = {
416 let inner = self.inner.read();
417 let arena = &inner.arena;
418 let gens = shared_generators(arena, &[self.raw_id(), other_id]);
419 let a = expr_to_multipoly(arena, self.raw_id(), &gens)?;
420 let b = expr_to_multipoly(arena, other_id, &gens)?;
421 (gens, a, b)
422 };
423 let result = op(&a, &b);
424 let id = {
425 let mut inner = self.inner.write();
426 multipoly_to_expr(&mut inner.arena, &result, &gens)
427 };
428 Some(self.wrap(id))
429 }
430
431 // ── Gröbner bases ──────────────────────────────────────────────
432
433 /// Reduced Gröbner basis of the ideal generated by `polys` in the
434 /// variables `vars` under the monomial order `order` (SymPy
435 /// `groebner(polys, *vars, order=…)`).
436 ///
437 /// Every element of the basis is monic; the list is sorted by leading
438 /// monomial, largest first. An empty `polys` (or all zeros) gives an
439 /// empty basis; an ideal containing a non-zero constant gives `[1]`.
440 /// `MonomialOrder::GrevLex` runs Buchberger directly;
441 /// `MonomialOrder::Lex` computes in grevlex first and converts with
442 /// FGLM when the ideal is zero-dimensional, which is much faster than
443 /// lex Buchberger and yields the same (unique) reduced basis.
444 ///
445 /// # Errors
446 ///
447 /// `InvalidArgument` if `vars` is empty, contains duplicates or
448 /// non-symbols, or if some polynomial is not a polynomial in `vars`
449 /// with rational coefficients (other symbols count as non-rational
450 /// coefficients).
451 ///
452 /// # Examples
453 ///
454 /// ```
455 /// use symplex::prelude::*;
456 /// use symplex::multipoly::MonomialOrder;
457 ///
458 /// let ctx = Context::new();
459 /// let (x, y) = (ctx.symbol("x"), ctx.symbol("y"));
460 /// let f = &x.powi(2) + &y.powi(2) - 1;
461 /// let g = &x - &y;
462 /// let vars = [x.clone(), y.clone()];
463 /// let basis = Ex::groebner(&[f, g.clone()], &vars, MonomialOrder::Lex).unwrap();
464 /// assert_eq!(basis, vec![g, &y.powi(2) - ctx.rational(1, 2)]);
465 /// ```
466 pub fn groebner(
467 polys: &[Ex],
468 vars: &[Ex],
469 order: MonomialOrder,
470 ) -> Result<Vec<Ex>, SymplexError> {
471 const OP: &str = "groebner";
472 let probe = vars
473 .first()
474 .ok_or_else(|| invalid(OP, "at least one variable is required"))?;
475 let var_ids = validate_vars(probe, vars, OP)?;
476 let poly_ids: Vec<ExprId> = polys.iter().map(|p| probe.checked_id(p)).collect();
477 // Convert under a read lock, compute unlocked, intern under a write
478 // lock.
479 let mps = {
480 let inner = probe.inner.read();
481 to_multipolys(&inner.arena, &poly_ids, &var_ids, OP)?
482 };
483 let basis = groebner_in_order(&mps, order);
484 let ids: Vec<ExprId> = {
485 let mut inner = probe.inner.write();
486 let arena = &mut inner.arena;
487 basis
488 .iter()
489 .map(|g| multipoly_to_expr(arena, g, &var_ids))
490 .collect()
491 };
492 Ok(ids.into_iter().map(|id| probe.wrap(id)).collect())
493 }
494
495 /// Remainder of `self` on multivariate division by `basis` in the
496 /// variables `vars` under `order` (SymPy `reduced(f, G)[1]` /
497 /// `GroebnerBasis.reduce`).
498 ///
499 /// When `basis` is a Gröbner basis for `order` this is the unique
500 /// normal form of `self` modulo the ideal — zero exactly when `self`
501 /// lies in the ideal. For an arbitrary `basis` it is *a* remainder,
502 /// which may depend on the order of the divisors.
503 ///
504 /// # Errors
505 ///
506 /// As [`groebner`](Self::groebner): invalid `vars`, or `self` or a
507 /// basis element that is not a polynomial in `vars` with rational
508 /// coefficients.
509 ///
510 /// # Examples
511 ///
512 /// ```
513 /// use symplex::prelude::*;
514 /// use symplex::multipoly::MonomialOrder;
515 ///
516 /// let ctx = Context::new();
517 /// let (x, y) = (ctx.symbol("x"), ctx.symbol("y"));
518 /// let vars = [x.clone(), y.clone()];
519 /// let basis = Ex::groebner(&[&x.powi(2) + &y.powi(2) - 1, &x - &y], &vars, MonomialOrder::Lex).unwrap();
520 /// // x² ≡ y² ≡ 1/2 modulo the ideal
521 /// let r = x.powi(2).reduce_modulo(&basis, &vars, MonomialOrder::Lex).unwrap();
522 /// assert_eq!(r, ctx.rational(1, 2));
523 /// assert!((&x.powi(2) - &y.powi(2)).reduce_modulo(&basis, &vars, MonomialOrder::Lex).unwrap().is_zero_structural());
524 /// ```
525 pub fn reduce_modulo(
526 &self,
527 basis: &[Ex],
528 vars: &[Ex],
529 order: MonomialOrder,
530 ) -> Result<Ex, SymplexError> {
531 const OP: &str = "reduce_modulo";
532 let var_ids = validate_vars(self, vars, OP)?;
533 let basis_ids: Vec<ExprId> = basis.iter().map(|b| self.checked_id(b)).collect();
534 // Convert under a read lock, compute unlocked, intern under a write
535 // lock.
536 let (f, divisors) = {
537 let inner = self.inner.read();
538 let arena = &inner.arena;
539 // `to_multipolys` returns one polynomial per input expression.
540 let Some(f) = to_multipolys(arena, &[self.raw_id()], &var_ids, OP)?
541 .into_iter()
542 .next()
543 else {
544 return Err(SymplexError::ComputationFailed {
545 operation: OP,
546 reason: "to_multipolys returned no polynomial for one input".into(),
547 });
548 };
549 (f, to_multipolys(arena, &basis_ids, &var_ids, OP)?)
550 };
551 let r = reduce_in_order(&f, &divisors, order);
552 let id = {
553 let mut inner = self.inner.write();
554 multipoly_to_expr(&mut inner.arena, &r, &var_ids)
555 };
556 Ok(self.wrap(id))
557 }
558
559 // ── Exact real roots ───────────────────────────────────────────
560
561 /// The distinct real roots of `self` as a polynomial in `var`, as
562 /// exact expressions in increasing order (SymPy `real_roots`, except
563 /// that a repeated root is listed once, as in
564 /// [`count_real_roots`](Self::count_real_roots)).
565 ///
566 /// Rational roots are returned as numbers. Every other root is a
567 /// `RootOf(g, k)` node, where `g` is the irreducible factor over ℤ
568 /// that vanishes there and `k` indexes `g`'s roots sorted by real then
569 /// imaginary part — the same node `solve` produces for degree ≥ 5, so
570 /// it evaluates numerically (`eval_f64`) and prints as `RootOf(…)`.
571 /// The order is decided exactly with Sturm sequences.
572 ///
573 /// Returns `None` if `self` is not a polynomial in `var` with rational
574 /// coefficients or is constant; a polynomial without real roots gives
575 /// `Some(vec![])`. `None` is also returned in the rare case that a
576 /// root cannot be *named* reliably — the factorisation over ℤ could
577 /// not be certified complete, or the `RootOf` index of a root is not
578 /// stable because another root of the same factor has the same real
579 /// part to within `2⁻⁶⁰` (the (re, im) order would then depend on
580 /// rounding). Every `RootOf(g, k)` that is returned has been checked
581 /// to evaluate inside the root's exact isolating interval.
582 ///
583 /// # Examples
584 ///
585 /// ```
586 /// use symplex::prelude::*;
587 ///
588 /// let ctx = Context::new();
589 /// let x = ctx.symbol("x");
590 /// let roots = (&x.powi(3) - &x * 2).real_roots(&x).unwrap(); // −√2, 0, √2
591 /// assert_eq!(roots.len(), 3);
592 /// assert_eq!(roots[1], ctx.int(0));
593 /// assert!((roots[2].eval_f64().unwrap() - 2f64.sqrt()).abs() < 1e-12);
594 /// assert!((&x.powi(2) + 1).real_roots(&x).unwrap().is_empty());
595 /// ```
596 #[must_use]
597 pub fn real_roots(&self, var: &Ex) -> Option<Vec<Ex>> {
598 let var_id = self.checked_id(var);
599 // Convert under a read lock, compute unlocked, intern under a write
600 // lock.
601 let f = {
602 let inner = self.inner.read();
603 expr_to_poly(&inner.arena, self.raw_id(), var_id)?
604 };
605 if f.degree().unwrap_or(0) == 0 {
606 return None;
607 }
608 let (factors, roots) = real_roots_of(&f)?;
609 let ids: Vec<ExprId> = {
610 let mut inner = self.inner.write();
611 let arena = &mut inner.arena;
612 let mut factor_ids: Vec<Option<ExprId>> = vec![None; factors.len()];
613 roots
614 .into_iter()
615 .map(|root| match root {
616 RealRoot::Rational(r) => arena.num_ratio(r),
617 RealRoot::RootOf { factor, index } => {
618 let g_expr = *factor_ids[factor]
619 .get_or_insert_with(|| poly_to_expr(arena, &factors[factor], var_id));
620 let idx = arena.int(index as i64);
621 arena.intern(ExprNode::RootOf(g_expr, idx))
622 }
623 })
624 .collect()
625 };
626 Some(ids.into_iter().map(|id| self.wrap(id)).collect())
627 }
628
629 /// The `index`-th distinct real root of `self` in `var`, counting from
630 /// the smallest (0-based) — `real_roots(var)[index]` (SymPy
631 /// `rootof(f, index)` for the real roots).
632 ///
633 /// `None` under the same conditions as [`real_roots`](Self::real_roots),
634 /// or if there are at most `index` real roots.
635 ///
636 /// # Examples
637 ///
638 /// ```
639 /// use symplex::prelude::*;
640 ///
641 /// let ctx = Context::new();
642 /// let x = ctx.symbol("x");
643 /// let f = &x.powi(5) - &x - 1; // one real root ≈ 1.1673
644 /// let r = f.root_of(&x, 0).unwrap();
645 /// assert!((r.eval_f64().unwrap() - 1.1673039782614187).abs() < 1e-12);
646 /// assert!(f.root_of(&x, 1).is_none());
647 /// ```
648 #[must_use]
649 pub fn root_of(&self, var: &Ex, index: usize) -> Option<Ex> {
650 self.real_roots(var)?.into_iter().nth(index)
651 }
652
653 // ── Factoring modulo a prime ───────────────────────────────────
654
655 /// Factorisation of `self`, a polynomial in `var`, over the prime
656 /// field `GF(p)` (SymPy `factor_list(f, modulus=p)`).
657 ///
658 /// Returns `(lc, [(factor, multiplicity), …])` with `self ≡ lc · ∏
659 /// factorᵢ^multᵢ (mod p)`: `lc` is the leading coefficient reduced mod
660 /// `p`, each factor is monic and irreducible over `GF(p)` with
661 /// coefficients in `[0, p)`, and the list is sorted by degree then
662 /// coefficients. Rational coefficients are reduced through the
663 /// inverse of their denominator. A polynomial that vanishes
664 /// identically mod `p` gives `(0, [])`; a constant gives `(c mod p,
665 /// [])`.
666 ///
667 /// # Errors
668 ///
669 /// `InvalidArgument` if `p` is not prime, if `p` is `2` or at least
670 /// `2³¹` (the finite-field arithmetic supports odd primes below
671 /// [`factor_zassenhaus::MAX_PRIME`](crate::factor_zassenhaus::MAX_PRIME)),
672 /// if `self` is not a polynomial in `var` with rational coefficients,
673 /// or if some coefficient has a denominator divisible by `p`.
674 ///
675 /// # Examples
676 ///
677 /// ```
678 /// use symplex::prelude::*;
679 ///
680 /// let ctx = Context::new();
681 /// let x = ctx.symbol("x");
682 /// // x² + 1 ≡ (x + 2)(x + 3) (mod 5)
683 /// let (lc, factors) = (&x.powi(2) + 1).factor_mod(&x, 5).unwrap();
684 /// assert_eq!(lc, ctx.int(1));
685 /// assert_eq!(factors, vec![(&x + 2, 1), (&x + 3, 1)]);
686 /// // … but irreducible mod 3.
687 /// let (_, factors) = (&x.powi(2) + 1).factor_mod(&x, 3).unwrap();
688 /// assert_eq!(factors, vec![(&x.powi(2) + 1, 1)]);
689 /// assert!((&x.powi(2) + 1).factor_mod(&x, 6).is_err());
690 /// ```
691 pub fn factor_mod(&self, var: &Ex, p: u64) -> Result<(Ex, Vec<(Ex, u32)>), SymplexError> {
692 const OP: &str = "factor_mod";
693 let var_id = self.checked_id(var);
694 if !crate::domains::ntheory::isprime(p) {
695 return Err(invalid(OP, format!("modulus {p} is not prime")));
696 }
697 if p == 2 || p >= crate::poly::factor_zassenhaus::MAX_PRIME {
698 return Err(invalid(
699 OP,
700 format!(
701 "modulus {p} is not supported: p must be an odd prime below {}",
702 crate::poly::factor_zassenhaus::MAX_PRIME
703 ),
704 ));
705 }
706 // Convert under a read lock, compute unlocked, intern under a write
707 // lock.
708 let f = {
709 let inner = self.inner.read();
710 expr_to_poly(&inner.arena, self.raw_id(), var_id).ok_or_else(|| {
711 invalid(
712 OP,
713 "expression is not a polynomial in the given variable with rational coefficients",
714 )
715 })?
716 };
717 let pb = BigInt::from(p);
718 if f.coeffs().iter().any(|c| (c.denom() % &pb).is_zero()) {
719 return Err(invalid(
720 OP,
721 format!(
722 "a coefficient has a denominator divisible by {p}, so it has no inverse mod {p}"
723 ),
724 ));
725 }
726 let (lc, factors) =
727 crate::poly::factor_zassenhaus::factor_mod_p(&f, p).ok_or_else(|| {
728 SymplexError::ComputationFailed {
729 operation: OP,
730 reason: "factor_mod_p rejected a valid modulus and polynomial".into(),
731 }
732 })?;
733 let (lc_id, factor_ids) = {
734 let mut inner = self.inner.write();
735 let arena = &mut inner.arena;
736 let lc_id = arena.int(lc as i64);
737 let factor_ids: Vec<(ExprId, u32)> = factors
738 .iter()
739 .map(|(coeffs, m)| {
740 let g = Poly::from_coeffs(
741 coeffs
742 .iter()
743 .map(|&c| Ratio::from_integer(BigInt::from(c)))
744 .collect(),
745 );
746 (poly_to_expr(arena, &g, var_id), *m)
747 })
748 .collect();
749 (lc_id, factor_ids)
750 };
751 Ok((
752 self.wrap(lc_id),
753 factor_ids
754 .into_iter()
755 .map(|(id, m)| (self.wrap(id), m))
756 .collect(),
757 ))
758 }
759
760 // ── Symbolic resultant / discriminant ──────────────────────────
761
762 /// Resultant `res_var(self, other)` of two polynomials in `var` whose
763 /// coefficients may be symbolic (SymPy `resultant(f, g, var)`).
764 ///
765 /// Computed as the determinant of the Sylvester matrix over `Ex`
766 /// entries and expanded, so the result is a polynomial in the
767 /// parameters; [`resultant`](Self::resultant) is the faster exact
768 /// route when every coefficient is rational. Returns `None` if either
769 /// expression is not polynomial in `var` (or `var` is not a symbol).
770 /// The resultant of two constants is `1`; if one polynomial is a
771 /// constant `c` and the other has degree `d`, the result is `c^d`.
772 ///
773 /// # Examples
774 ///
775 /// ```
776 /// use symplex::prelude::*;
777 ///
778 /// let ctx = Context::new();
779 /// let (x, a, b) = (ctx.symbol("x"), ctx.symbol("a"), ctx.symbol("b"));
780 /// // res(x − a, x − b) = g(a) = a − b
781 /// let r = (&x - &a).resultant_symbolic(&(&x - &b), &x).unwrap();
782 /// assert_eq!(r, &a - &b);
783 /// // res(x² + a, x + b) = a + b²
784 /// let r = (&x.powi(2) + &a).resultant_symbolic(&(&x + &b), &x).unwrap();
785 /// assert_eq!(r, &a + &b.powi(2));
786 /// ```
787 #[must_use]
788 pub fn resultant_symbolic(&self, other: &Ex, var: &Ex) -> Option<Ex> {
789 let _ = self.checked_id(other);
790 let _ = self.checked_id(var);
791 let f = crate::api::poly_ex::Poly::new(self, &[var])?;
792 let g = crate::api::poly_ex::Poly::new(other, &[var])?;
793 if f.is_zero() || g.is_zero() {
794 return Some(self.context().zero());
795 }
796 let fc = f.all_coeffs()?;
797 let gc = g.all_coeffs()?;
798 sylvester_resultant(&self.context(), &fc, &gc)
799 }
800
801 /// Discriminant of `self` as a polynomial in `var` with possibly
802 /// symbolic coefficients (SymPy `discriminant(f, var)`).
803 ///
804 /// `disc(f) = (−1)^{n(n−1)/2} · res(f, f′) / lc(f)`, evaluated
805 /// division-free: the leading coefficient is eliminated from the
806 /// Sylvester matrix of `f` and `f′` by one row operation before taking
807 /// the determinant, so the result is an expanded polynomial in the
808 /// parameters (`b² − 4ac` for `ax² + bx + c`). Returns `None` for
809 /// non-polynomial or constant input; a linear polynomial has
810 /// discriminant `1`.
811 ///
812 /// # Examples
813 ///
814 /// ```
815 /// use symplex::prelude::*;
816 ///
817 /// let ctx = Context::new();
818 /// let (x, a, b, c) = (ctx.symbol("x"), ctx.symbol("a"), ctx.symbol("b"), ctx.symbol("c"));
819 /// let quad = &a * &x.powi(2) + &b * &x + &c;
820 /// assert_eq!(quad.discriminant_symbolic(&x).unwrap(), &b.powi(2) - &a * &c * 4);
821 /// // depressed cubic x³ + px + q: −4p³ − 27q²
822 /// let (p, q) = (ctx.symbol("p"), ctx.symbol("q"));
823 /// let cubic = &x.powi(3) + &p * &x + &q;
824 /// assert_eq!(cubic.discriminant_symbolic(&x).unwrap(), -(&p.powi(3) * 4) - &q.powi(2) * 27);
825 /// ```
826 #[must_use]
827 pub fn discriminant_symbolic(&self, var: &Ex) -> Option<Ex> {
828 let _ = self.checked_id(var);
829 let f = crate::api::poly_ex::Poly::new(self, &[var])?;
830 let coeffs = f.all_coeffs()?; // highest degree first
831 let n = coeffs.len().checked_sub(1)?;
832 if n == 0 {
833 return None;
834 }
835 let ctx = self.context();
836 if n == 1 {
837 return Some(ctx.one());
838 }
839 // Sylvester matrix of f (n + 1 coefficients) and f′ (n coefficients):
840 // (2n − 1) × (2n − 1), with n − 1 rows of f above n rows of f′.
841 // The first column is a_n at row 0 and n·a_n at row n − 1; the row
842 // operation R_{n−1} ← R_{n−1} − n·R_0 leaves a_n alone in the
843 // column, so det = a_n · det(minor) and the minor gives disc up to
844 // the sign (−1)^{n(n−1)/2}.
845 let deriv: Vec<Ex> = coeffs[..n]
846 .iter()
847 .enumerate()
848 .map(|(j, c)| c * ((n - j) as i64))
849 .collect();
850 let size = 2 * n - 1;
851 let zero = ctx.zero();
852 let mut rows: Vec<Vec<Ex>> = Vec::with_capacity(size);
853 for i in 0..n - 1 {
854 let mut row = vec![zero.clone(); size];
855 for (j, c) in coeffs.iter().enumerate() {
856 row[i + j] = c.clone();
857 }
858 rows.push(row);
859 }
860 for i in 0..n {
861 let mut row = vec![zero.clone(); size];
862 for (j, c) in deriv.iter().enumerate() {
863 row[i + j] = c.clone();
864 }
865 rows.push(row);
866 }
867 // R_{n−1} − n·R_0 = [0, −a_{n−1}, −2a_{n−2}, …, −n·a_0, 0, …]:
868 // entry j is −j · coeffs[j] for 1 ≤ j ≤ n.
869 for (j, (slot, c)) in rows[n - 1].iter_mut().zip(coeffs.iter()).enumerate() {
870 *slot = -&(c * (j as i64));
871 }
872 // Drop row 0 and column 0.
873 let minor: Vec<Vec<Ex>> = rows[1..].iter().map(|row| row[1..].to_vec()).collect();
874 // Invariant: `minor` is a square `(2n − 2) × (2n − 2)` matrix with
875 // `n ≥ 2`, so construction and `det` cannot fail; an `Err` would be
876 // an internal error, folded into `None` because the return type
877 // cannot carry it (see `sylvester_resultant`).
878 let matrix = crate::domains::matrix::Matrix::new(minor);
879 debug_assert!(
880 matrix.is_ok(),
881 "discriminant minor is square by construction"
882 );
883 let det = matrix.ok()?.det();
884 debug_assert!(det.is_ok(), "determinant of a square matrix over Ex");
885 let det = det.ok()?.expand();
886 Some(if (n * (n - 1) / 2) % 2 == 1 {
887 -det
888 } else {
889 det
890 })
891 }
892}