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 2: Normalize leading coefficient of denominator.
207        // For IntegerDomain/RationalDomain: ensure positive leading coefficient.
208        // For FiniteField: ensure leading coefficient is 1.
209        if let Some(den_lc) = self.denominator.leading_coeff() {
210            // If leading coefficient is "negative" (check via domain), negate both.
211            // For domains without ordering, we just ensure consistency.
212            if let Some(neg_lc) = self.numerator.domain().inv(den_lc) {
213                // FiniteField or field: divide both by leading coeff to make den monic.
214                self.numerator = self.numerator.mul_scalar(&neg_lc);
215                self.denominator = self.denominator.mul_scalar(&neg_lc);
216            }
217        }
218    }
219
220    // ------------------------------------------------------------------
221    //  Arithmetic (EuclideanDomain required for GCD-based operations)
222    // ------------------------------------------------------------------
223
224    /// Add two rational polynomials: $\frac{a}{b} + \frac{c}{d}$.
225    ///
226    /// Uses the denominator-GCD strategy to minimize intermediate growth.
227    pub fn add(&self, other: &Self) -> Self {
228        if self.is_zero() {
229            return other.clone();
230        }
231        if other.is_zero() {
232            return self.clone();
233        }
234
235        // Check for same denominator (common case).
236        if self.denominator == other.denominator {
237            let num = self.numerator.add(&other.numerator);
238            return Self::from_num_den(num, self.denominator.clone());
239        }
240
241        // General case: cross-multiply then canonicalize.
242        // TODO: optimize with denominator GCD strategy (Symbolica-style).
243        let ad = self.numerator.mul(&other.denominator);
244        let bc = other.numerator.mul(&self.denominator);
245        let num = ad.add(&bc);
246        let den = self.denominator.mul(&other.denominator);
247        Self::from_num_den(num, den)
248    }
249
250    /// Subtract two rational polynomials.
251    pub fn sub(&self, other: &Self) -> Self {
252        self.add(&other.neg())
253    }
254
255    /// Multiply two rational polynomials with cross-cancellation.
256    ///
257    /// Computes $\gcd(a, d)$ and $\gcd(b, c)$ before multiplying to
258    /// reduce intermediate coefficient growth.
259    pub fn mul(&self, other: &Self) -> Self {
260        if self.is_zero() || other.is_zero() {
261            return Self::zero(self.domain(), self.n_vars());
262        }
263
264        // Cross-cancellation: gcd(num1, den2) and gcd(den1, num2).
265        // For generic domains, fall back to simple multiply + canonicalize.
266        // TODO: implement cross-cancellation with multivariate GCD.
267        let num = self.numerator.mul(&other.numerator);
268        let den = self.denominator.mul(&other.denominator);
269        Self::from_num_den(num, den)
270    }
271
272    /// Divide two rational polynomials: $\frac{a/b}{c/d} = \frac{ad}{bc}$.
273    pub fn div(&self, other: &Self) -> Option<Self> {
274        let inv = other.inv()?;
275        Some(self.mul(&inv))
276    }
277}
278
279// ------------------------------------------------------------------
280//  Display
281// ------------------------------------------------------------------
282
283impl<D: Domain, O: MonomialOrder> fmt::Display for RationalPolynomial<D, O>
284where
285    D::Element: fmt::Display,
286{
287    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
288        if self.denominator.is_zero() || self.denominator.n_terms() <= 1 {
289            // Check if denominator is constant 1.
290            let const_val = self.denominator.coeff(&vec![0; self.denominator.n_vars()]);
291            if self.domain().is_one(&const_val) {
292                return write!(f, "{:?}", self.numerator);
293            }
294        }
295        write!(f, "({:?}) / ({:?})", self.numerator, self.denominator)
296    }
297}
298
299// ------------------------------------------------------------------
300//  Helper: div_scalar on SparseMultivariatePolynomial
301// ------------------------------------------------------------------
302
303impl<D: EuclideanDomain, O: MonomialOrder> SparseMultivariatePolynomial<D, O> {
304    /// Divide all coefficients by a scalar (must divide exactly).
305    fn div_scalar(&self, scalar: &D::Element) -> Self {
306        if self.domain().is_one(scalar) {
307            return self.clone();
308        }
309        let inv = self
310            .domain()
311            .inv(scalar)
312            .expect("div_scalar: cannot invert zero");
313        self.mul_scalar(&inv)
314    }
315}
316
317// ------------------------------------------------------------------
318//  Tests
319// ------------------------------------------------------------------
320
321#[cfg(test)]
322mod tests {
323    use super::*;
324    use crate::sparse::Lex;
325    use ocas_domain::{Integer, IntegerDomain};
326
327    type ZPoly = SparseMultivariatePolynomial<IntegerDomain, Lex>;
328    type ZRat = RationalPolynomial<IntegerDomain, Lex>;
329
330    fn poly1(terms: Vec<(Vec<usize>, i64)>) -> ZPoly {
331        ZPoly::from_terms(
332            IntegerDomain,
333            1,
334            terms
335                .into_iter()
336                .map(|(e, c)| (e, Integer::from(c)))
337                .collect(),
338        )
339    }
340
341    #[allow(dead_code)]
342    fn poly2(terms: Vec<(Vec<usize>, i64)>, n_vars: usize) -> ZPoly {
343        ZPoly::from_terms(
344            IntegerDomain,
345            n_vars,
346            terms
347                .into_iter()
348                .map(|(e, c)| (e, Integer::from(c)))
349                .collect(),
350        )
351    }
352
353    #[test]
354    fn rational_zero_and_one() {
355        let z = ZRat::zero(&IntegerDomain, 1);
356        assert!(z.is_zero());
357        assert!(!z.is_one());
358
359        let o = ZRat::one(&IntegerDomain, 1);
360        assert!(!o.is_zero());
361        assert!(o.is_one());
362    }
363
364    #[test]
365    fn rational_from_polynomial() {
366        // p = x + 1
367        let p = poly1(vec![(vec![0], 1), (vec![1], 1)]);
368        let r = ZRat::from_polynomial(p.clone());
369        assert_eq!(r.numerator, p);
370        assert!(r.denominator.n_terms() <= 1);
371    }
372
373    #[test]
374    fn rational_neg() {
375        // r = x / (x + 1)
376        let num = poly1(vec![(vec![1], 1)]);
377        let den = poly1(vec![(vec![0], 1), (vec![1], 1)]);
378        let r = ZRat::new(num, den);
379        let nr = r.neg();
380        // -x / (x+1)
381        assert_eq!(nr.numerator.coeff(&[1]), Integer::from(-1));
382    }
383
384    #[test]
385    fn rational_add_same_den() {
386        // 1/x + 1/x = 2/x
387        let x = poly1(vec![(vec![1], 1)]);
388        let one = poly1(vec![(vec![0], 1)]);
389
390        let r1 = ZRat::new(one.clone(), x.clone());
391        let r2 = ZRat::new(one, x.clone());
392        let sum = r1.add(&r2);
393
394        // After canonicalization: 2/x
395        assert_eq!(sum.numerator.coeff(&[0]), Integer::from(2));
396    }
397
398    #[test]
399    fn rational_add_different_den() {
400        // 1/(x-1) + 1/(x+1) = 2x / (x^2-1)
401        // x-1 = [-1, 1]  (constant -1, coeff of x is 1)
402        let x_minus_1 = poly1(vec![(vec![0], -1), (vec![1], 1)]);
403        let x_plus_1 = poly1(vec![(vec![0], 1), (vec![1], 1)]);
404        let one = poly1(vec![(vec![0], 1)]);
405
406        let r1 = ZRat::new(one.clone(), x_minus_1);
407        let r2 = ZRat::new(one, x_plus_1);
408        let sum = r1.add(&r2);
409
410        // Result should be non-zero.
411        assert!(!sum.is_zero());
412        // Verify by multiplying back: sum * den should equal num.
413    }
414
415    #[test]
416    fn rational_mul() {
417        // (x+1)/(x-1) * (x-1)/(x+1) = 1
418        let x_plus_1 = poly1(vec![(vec![0], 1), (vec![1], 1)]);
419        let x_minus_1 = poly1(vec![(vec![0], -1), (vec![1], 1)]);
420
421        let r1 = ZRat::new(x_plus_1.clone(), x_minus_1.clone());
422        let r2 = ZRat::new(x_minus_1, x_plus_1);
423        let prod = r1.mul(&r2);
424
425        // Should canonicalize to 1/1.
426        assert!(prod.is_one() || (prod.numerator == prod.denominator));
427    }
428
429    #[test]
430    fn rational_inv() {
431        let x = poly1(vec![(vec![1], 1)]);
432        let one = poly1(vec![(vec![0], 1)]);
433        let r = ZRat::new(x, one);
434        let r_inv = r.inv().unwrap();
435
436        // inv(x/1) = 1/x
437        assert_eq!(r_inv.numerator, r_inv.denominator.one());
438    }
439
440    #[test]
441    fn rational_pow() {
442        // (x/1)^3 = x^3/1
443        let x = poly1(vec![(vec![1], 1)]);
444        let one = poly1(vec![(vec![0], 1)]);
445        let r = ZRat::new(x, one);
446        let r3 = r.pow(3);
447
448        // numerator should be x^3
449        assert_eq!(r3.numerator.coeff(&[3]), Integer::from(1));
450        assert_eq!(r3.numerator.n_terms(), 1);
451    }
452}