Skip to main content

ocas_poly/
rational.rs

1//! Rational polynomials: numerator / denominator pairs with GCD-based reduction.
2//!
3//! A [`RationalPolynomial`] represents an element of the fraction field of a
4//! polynomial ring. The numerator and denominator are stored as
5//! [`SparseMultivariatePolynomial`] and kept in canonical form (coprime,
6//! positive leading coefficient on the denominator).
7//!
8//! Arithmetic follows Symbolica's strategy: addition uses a denominator-GCD
9//! first approach, and multiplication uses cross-cancellation to avoid
10//! intermediate coefficient growth.
11
12use std::fmt;
13
14use ocas_domain::{Domain, EuclideanDomain};
15
16use crate::sparse::{Grevlex, MonomialOrder, SparseMultivariatePolynomial};
17
18/// A rational polynomial $\frac{\text{num}}{\text{den}}$ over a domain `D`.
19///
20/// After construction via [`from_num_den`](Self::from_num_den), the fraction
21/// is always in canonical form:
22/// - numerator and denominator are coprime,
23/// - the denominator's leading coefficient is positive (for ordered domains)
24///   or equal to 1 (for finite fields).
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct RationalPolynomial<D: Domain, O: MonomialOrder = Grevlex> {
27    /// The numerator polynomial.
28    pub numerator: SparseMultivariatePolynomial<D, O>,
29    /// The denominator polynomial (always non-zero).
30    pub denominator: SparseMultivariatePolynomial<D, O>,
31}
32
33impl<D: Domain, O: MonomialOrder> RationalPolynomial<D, O> {
34    // ------------------------------------------------------------------
35    //  Constructors
36    // ------------------------------------------------------------------
37
38    /// Create a rational polynomial without reduction.
39    ///
40    /// The caller must ensure `denominator` is non-zero. For a canonicalized
41    /// version use [`from_num_den`](Self::from_num_den).
42    pub fn new(
43        numerator: SparseMultivariatePolynomial<D, O>,
44        denominator: SparseMultivariatePolynomial<D, O>,
45    ) -> Self {
46        debug_assert!(
47            !denominator.is_zero(),
48            "RationalPolynomial: denominator must be non-zero"
49        );
50        Self {
51            numerator,
52            denominator,
53        }
54    }
55
56    /// Create a rational polynomial from a polynomial (denominator = 1).
57    pub fn from_polynomial(poly: SparseMultivariatePolynomial<D, O>) -> Self {
58        let one = poly.one();
59        Self {
60            numerator: poly,
61            denominator: one,
62        }
63    }
64
65    /// Return the zero rational polynomial in `n_vars` variables.
66    pub fn zero(domain: &D, n_vars: usize) -> Self {
67        let z = SparseMultivariatePolynomial::new(domain.clone(), n_vars);
68        let one = z.one();
69        Self {
70            numerator: z.clone(),
71            denominator: one,
72        }
73    }
74
75    /// Return the unit rational polynomial (1/1) in `n_vars` variables.
76    pub fn one(domain: &D, n_vars: usize) -> Self {
77        let o = SparseMultivariatePolynomial::new(domain.clone(), n_vars).one();
78        Self {
79            numerator: o.clone(),
80            denominator: o,
81        }
82    }
83
84    /// Return whether this is the zero rational polynomial.
85    pub fn is_zero(&self) -> bool {
86        self.numerator.is_zero()
87    }
88
89    /// Return whether this is the unit rational polynomial (1/1).
90    pub fn is_one(&self) -> bool {
91        self.numerator == self.denominator
92    }
93
94    /// Return the number of variables.
95    pub fn n_vars(&self) -> usize {
96        self.numerator.n_vars()
97    }
98
99    /// Return a reference to the coefficient domain.
100    pub fn domain(&self) -> &D {
101        self.numerator.domain()
102    }
103
104    /// Return the negation: $-\frac{n}{d}$.
105    pub fn neg(&self) -> Self {
106        Self {
107            numerator: self.numerator.neg(),
108            denominator: self.denominator.clone(),
109        }
110    }
111
112    /// Return the multiplicative inverse: $\frac{d}{n}$.
113    ///
114    /// Returns `None` if the numerator is zero.
115    pub fn inv(&self) -> Option<Self> {
116        if self.numerator.is_zero() {
117            return None;
118        }
119        Some(Self {
120            numerator: self.denominator.clone(),
121            denominator: self.numerator.clone(),
122        })
123    }
124
125    /// Return the power $\left(\frac{n}{d}\right)^k$.
126    pub fn pow(&self, k: u32) -> Self {
127        if k == 0 {
128            return Self::one(self.domain(), self.n_vars());
129        }
130        // Simple repeated squaring on numerator and denominator separately.
131        let mut num = self.numerator.one();
132        let mut den = self.denominator.one();
133        let mut base_num = self.numerator.clone();
134        let mut base_den = self.denominator.clone();
135        let mut exp = k;
136        while exp > 0 {
137            if exp & 1 == 1 {
138                num = num.mul(&base_num);
139                den = den.mul(&base_den);
140            }
141            base_num = base_num.mul(&base_num);
142            base_den = base_den.mul(&base_den);
143            exp >>= 1;
144        }
145        Self {
146            numerator: num,
147            denominator: den,
148        }
149    }
150}
151
152impl<D: EuclideanDomain, O: MonomialOrder> RationalPolynomial<D, O> {
153    // ------------------------------------------------------------------
154    //  Canonicalized constructors
155    // ------------------------------------------------------------------
156
157    /// Create a canonicalized rational polynomial from numerator and
158    /// denominator.
159    ///
160    /// The result has coprime numerator and denominator, with the
161    /// denominator's leading coefficient normalized.
162    pub fn from_num_den(
163        numerator: SparseMultivariatePolynomial<D, O>,
164        denominator: SparseMultivariatePolynomial<D, O>,
165    ) -> Self {
166        if denominator.is_zero() {
167            panic!("RationalPolynomial::from_num_den: denominator is zero");
168        }
169        if numerator.is_zero() {
170            return Self {
171                numerator,
172                denominator,
173            };
174        }
175        let mut rat = Self {
176            numerator,
177            denominator,
178        };
179        rat.canonicalize();
180        rat
181    }
182
183    /// Reduce the fraction to canonical form.
184    ///
185    /// 1. Compute $\gcd(\text{num}, \text{den})$ and divide both.
186    /// 2. Normalize the denominator's leading coefficient.
187    fn canonicalize(&mut self) {
188        if self.numerator.is_zero() {
189            return;
190        }
191        // Step 1: GCD reduction.
192        // Use the multivariate GCD infrastructure. For bivariate polynomials
193        // we use bivariate_gcd; for general case we use gcd_modular.
194        // Both operate on IntegerDomain/Lex, so we need to handle the generic
195        // case differently. For now, use a simple approach: compute content
196        // GCD and primitive parts.
197        let num_content = self.numerator.content();
198        let den_content = self.denominator.content();
199        let coeff_gcd = self.numerator.domain().gcd(&num_content, &den_content);
200
201        if !self.numerator.domain().is_one(&coeff_gcd) {
202            self.numerator = self.numerator.div_scalar(&coeff_gcd);
203            self.denominator = self.denominator.div_scalar(&coeff_gcd);
204        }
205
206        // Step 1.5: exact polynomial GCD reduction for univariate
207        // polynomials, via the dense Euclidean algorithm (generic over any
208        // EuclideanDomain).
209        if self.numerator.n_vars() == 1 {
210            let num_d = sparse_to_dense_uni(&self.numerator);
211            let den_d = sparse_to_dense_uni(&self.denominator);
212            let g = num_d.gcd(&den_d);
213            if g.degree().unwrap_or(0) > 0
214                && let (Some((nq, r1)), Some((dq, r2))) = (num_d.div_rem(&g), den_d.div_rem(&g))
215            {
216                debug_assert!(r1.is_zero() && r2.is_zero());
217                self.numerator = dense_to_sparse_uni(&nq);
218                self.denominator = dense_to_sparse_uni(&dq);
219            }
220        }
221
222        // Step 2: Normalize leading coefficient of denominator.
223        // For IntegerDomain/RationalDomain: ensure positive leading coefficient.
224        // For FiniteField: ensure leading coefficient is 1.
225        if let Some(den_lc) = self.denominator.leading_coeff() {
226            // If leading coefficient is "negative" (check via domain), negate both.
227            // For domains without ordering, we just ensure consistency.
228            if let Some(neg_lc) = self.numerator.domain().inv(den_lc) {
229                // FiniteField or field: divide both by leading coeff to make den monic.
230                self.numerator = self.numerator.mul_scalar(&neg_lc);
231                self.denominator = self.denominator.mul_scalar(&neg_lc);
232            }
233        }
234    }
235
236    // ------------------------------------------------------------------
237    //  Arithmetic (EuclideanDomain required for GCD-based operations)
238    // ------------------------------------------------------------------
239
240    /// Add two rational polynomials: $\frac{a}{b} + \frac{c}{d}$.
241    ///
242    /// Uses the denominator-GCD strategy to minimize intermediate growth.
243    pub fn add(&self, other: &Self) -> Self {
244        if self.is_zero() {
245            return other.clone();
246        }
247        if other.is_zero() {
248            return self.clone();
249        }
250
251        // Check for same denominator (common case).
252        if self.denominator == other.denominator {
253            let num = self.numerator.add(&other.numerator);
254            return Self::from_num_den(num, self.denominator.clone());
255        }
256
257        // General case: cross-multiply then canonicalize.
258        // TODO: optimize with denominator GCD strategy (Symbolica-style).
259        let ad = self.numerator.mul(&other.denominator);
260        let bc = other.numerator.mul(&self.denominator);
261        let num = ad.add(&bc);
262        let den = self.denominator.mul(&other.denominator);
263        Self::from_num_den(num, den)
264    }
265
266    /// Subtract two rational polynomials.
267    pub fn sub(&self, other: &Self) -> Self {
268        self.add(&other.neg())
269    }
270
271    /// Multiply two rational polynomials with cross-cancellation.
272    ///
273    /// Computes $\gcd(a, d)$ and $\gcd(b, c)$ before multiplying to
274    /// reduce intermediate coefficient growth.
275    pub fn mul(&self, other: &Self) -> Self {
276        if self.is_zero() || other.is_zero() {
277            return Self::zero(self.domain(), self.n_vars());
278        }
279
280        // Cross-cancellation: gcd(num1, den2) and gcd(den1, num2).
281        // For generic domains, fall back to simple multiply + canonicalize.
282        // TODO: implement cross-cancellation with multivariate GCD.
283        let num = self.numerator.mul(&other.numerator);
284        let den = self.denominator.mul(&other.denominator);
285        Self::from_num_den(num, den)
286    }
287
288    /// Divide two rational polynomials: $\frac{a/b}{c/d} = \frac{ad}{bc}$.
289    pub fn div(&self, other: &Self) -> Option<Self> {
290        let inv = other.inv()?;
291        Some(self.mul(&inv))
292    }
293}
294
295// ------------------------------------------------------------------
296//  Display
297// ------------------------------------------------------------------
298
299impl<D: Domain, O: MonomialOrder> fmt::Display for RationalPolynomial<D, O>
300where
301    D::Element: fmt::Display,
302{
303    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
304        if self.denominator.is_zero() || self.denominator.n_terms() <= 1 {
305            // Check if denominator is constant 1.
306            let const_val = self.denominator.coeff(&vec![0; self.denominator.n_vars()]);
307            if self.domain().is_one(&const_val) {
308                return write!(f, "{:?}", self.numerator);
309            }
310        }
311        write!(f, "({:?}) / ({:?})", self.numerator, self.denominator)
312    }
313}
314
315// ------------------------------------------------------------------
316//  Helper: div_scalar on SparseMultivariatePolynomial
317// ------------------------------------------------------------------
318
319impl<D: EuclideanDomain, O: MonomialOrder> SparseMultivariatePolynomial<D, O> {
320    /// Divide all coefficients by a scalar (must divide exactly).
321    fn div_scalar(&self, scalar: &D::Element) -> Self {
322        if self.domain().is_one(scalar) {
323            return self.clone();
324        }
325        let inv = self
326            .domain()
327            .inv(scalar)
328            .expect("div_scalar: cannot invert zero");
329        self.mul_scalar(&inv)
330    }
331}
332
333// ------------------------------------------------------------------
334//  Univariate conversion helpers (for exact GCD canonicalization)
335// ------------------------------------------------------------------
336
337/// Convert a univariate sparse polynomial to dense form.
338fn sparse_to_dense_uni<D: EuclideanDomain, O: MonomialOrder>(
339    p: &SparseMultivariatePolynomial<D, O>,
340) -> crate::DenseUnivariatePolynomial<D> {
341    debug_assert_eq!(p.n_vars(), 1);
342    let deg = p.degree_in(0);
343    let mut coeffs = vec![p.domain().zero(); deg + 1];
344    for (exp, coeff) in p.terms_ref() {
345        coeffs[exp[0]] = coeff.clone();
346    }
347    crate::DenseUnivariatePolynomial::from_coeffs(p.domain().clone(), coeffs)
348}
349
350/// Convert a dense polynomial to univariate sparse form.
351fn dense_to_sparse_uni<D: EuclideanDomain, O: MonomialOrder>(
352    p: &crate::DenseUnivariatePolynomial<D>,
353) -> SparseMultivariatePolynomial<D, O> {
354    let terms = p
355        .coeffs()
356        .iter()
357        .enumerate()
358        .filter(|&(_, c)| !p.domain().is_zero(c))
359        .map(|(i, c)| (vec![i], c.clone()))
360        .collect();
361    SparseMultivariatePolynomial::from_terms(p.domain().clone(), 1, terms)
362}
363
364// ------------------------------------------------------------------
365//  Tests
366// ------------------------------------------------------------------
367
368#[cfg(test)]
369mod tests {
370    use super::*;
371    use crate::sparse::Lex;
372    use ocas_domain::{Integer, IntegerDomain};
373
374    type ZPoly = SparseMultivariatePolynomial<IntegerDomain, Lex>;
375    type ZRat = RationalPolynomial<IntegerDomain, Lex>;
376
377    fn poly1(terms: Vec<(Vec<usize>, i64)>) -> ZPoly {
378        ZPoly::from_terms(
379            IntegerDomain,
380            1,
381            terms
382                .into_iter()
383                .map(|(e, c)| (e, Integer::from(c)))
384                .collect(),
385        )
386    }
387
388    #[allow(dead_code)]
389    fn poly2(terms: Vec<(Vec<usize>, i64)>, n_vars: usize) -> ZPoly {
390        ZPoly::from_terms(
391            IntegerDomain,
392            n_vars,
393            terms
394                .into_iter()
395                .map(|(e, c)| (e, Integer::from(c)))
396                .collect(),
397        )
398    }
399
400    #[test]
401    fn rational_zero_and_one() {
402        let z = ZRat::zero(&IntegerDomain, 1);
403        assert!(z.is_zero());
404        assert!(!z.is_one());
405
406        let o = ZRat::one(&IntegerDomain, 1);
407        assert!(!o.is_zero());
408        assert!(o.is_one());
409    }
410
411    #[test]
412    fn rational_from_polynomial() {
413        // p = x + 1
414        let p = poly1(vec![(vec![0], 1), (vec![1], 1)]);
415        let r = ZRat::from_polynomial(p.clone());
416        assert_eq!(r.numerator, p);
417        assert!(r.denominator.n_terms() <= 1);
418    }
419
420    #[test]
421    fn rational_neg() {
422        // r = x / (x + 1)
423        let num = poly1(vec![(vec![1], 1)]);
424        let den = poly1(vec![(vec![0], 1), (vec![1], 1)]);
425        let r = ZRat::new(num, den);
426        let nr = r.neg();
427        // -x / (x+1)
428        assert_eq!(nr.numerator.coeff(&[1]), Integer::from(-1));
429    }
430
431    #[test]
432    fn rational_add_same_den() {
433        // 1/x + 1/x = 2/x
434        let x = poly1(vec![(vec![1], 1)]);
435        let one = poly1(vec![(vec![0], 1)]);
436
437        let r1 = ZRat::new(one.clone(), x.clone());
438        let r2 = ZRat::new(one, x.clone());
439        let sum = r1.add(&r2);
440
441        // After canonicalization: 2/x
442        assert_eq!(sum.numerator.coeff(&[0]), Integer::from(2));
443    }
444
445    #[test]
446    fn rational_add_different_den() {
447        // 1/(x-1) + 1/(x+1) = 2x / (x^2-1)
448        // x-1 = [-1, 1]  (constant -1, coeff of x is 1)
449        let x_minus_1 = poly1(vec![(vec![0], -1), (vec![1], 1)]);
450        let x_plus_1 = poly1(vec![(vec![0], 1), (vec![1], 1)]);
451        let one = poly1(vec![(vec![0], 1)]);
452
453        let r1 = ZRat::new(one.clone(), x_minus_1);
454        let r2 = ZRat::new(one, x_plus_1);
455        let sum = r1.add(&r2);
456
457        // Result should be non-zero.
458        assert!(!sum.is_zero());
459        // Verify by multiplying back: sum * den should equal num.
460    }
461
462    #[test]
463    fn rational_mul() {
464        // (x+1)/(x-1) * (x-1)/(x+1) = 1
465        let x_plus_1 = poly1(vec![(vec![0], 1), (vec![1], 1)]);
466        let x_minus_1 = poly1(vec![(vec![0], -1), (vec![1], 1)]);
467
468        let r1 = ZRat::new(x_plus_1.clone(), x_minus_1.clone());
469        let r2 = ZRat::new(x_minus_1, x_plus_1);
470        let prod = r1.mul(&r2);
471
472        // Should canonicalize to 1/1.
473        assert!(prod.is_one() || (prod.numerator == prod.denominator));
474    }
475
476    #[test]
477    fn rational_inv() {
478        let x = poly1(vec![(vec![1], 1)]);
479        let one = poly1(vec![(vec![0], 1)]);
480        let r = ZRat::new(x, one);
481        let r_inv = r.inv().unwrap();
482
483        // inv(x/1) = 1/x
484        assert_eq!(r_inv.numerator, r_inv.denominator.one());
485    }
486
487    #[test]
488    fn rational_pow() {
489        // (x/1)^3 = x^3/1
490        let x = poly1(vec![(vec![1], 1)]);
491        let one = poly1(vec![(vec![0], 1)]);
492        let r = ZRat::new(x, one);
493        let r3 = r.pow(3);
494
495        // numerator should be x^3
496        assert_eq!(r3.numerator.coeff(&[3]), Integer::from(1));
497        assert_eq!(r3.numerator.n_terms(), 1);
498    }
499}