ocas-poly 0.13.1

Polynomial algorithms for oCAS
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
//! Rational polynomials: numerator / denominator pairs with GCD-based reduction.
//!
//! A [`RationalPolynomial`] represents an element of the fraction field of a
//! polynomial ring. The numerator and denominator are stored as
//! [`SparseMultivariatePolynomial`] and kept in canonical form (coprime,
//! positive leading coefficient on the denominator).
//!
//! Arithmetic follows Symbolica's strategy: addition uses a denominator-GCD
//! first approach, and multiplication uses cross-cancellation to avoid
//! intermediate coefficient growth.

use std::fmt;

use ocas_domain::{Domain, EuclideanDomain};

use crate::sparse::{Grevlex, MonomialOrder, SparseMultivariatePolynomial};

/// A rational polynomial $\frac{\text{num}}{\text{den}}$ over a domain `D`.
///
/// After construction via [`from_num_den`](Self::from_num_den), the fraction
/// is always in canonical form:
/// - numerator and denominator are coprime,
/// - the denominator's leading coefficient is positive (for ordered domains)
///   or equal to 1 (for finite fields).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RationalPolynomial<D: Domain, O: MonomialOrder = Grevlex> {
    /// The numerator polynomial.
    pub numerator: SparseMultivariatePolynomial<D, O>,
    /// The denominator polynomial (always non-zero).
    pub denominator: SparseMultivariatePolynomial<D, O>,
}

impl<D: Domain, O: MonomialOrder> RationalPolynomial<D, O> {
    // ------------------------------------------------------------------
    //  Constructors
    // ------------------------------------------------------------------

    /// Create a rational polynomial without reduction.
    ///
    /// The caller must ensure `denominator` is non-zero. For a canonicalized
    /// version use [`from_num_den`](Self::from_num_den).
    pub fn new(
        numerator: SparseMultivariatePolynomial<D, O>,
        denominator: SparseMultivariatePolynomial<D, O>,
    ) -> Self {
        debug_assert!(
            !denominator.is_zero(),
            "RationalPolynomial: denominator must be non-zero"
        );
        Self {
            numerator,
            denominator,
        }
    }

    /// Create a rational polynomial from a polynomial (denominator = 1).
    pub fn from_polynomial(poly: SparseMultivariatePolynomial<D, O>) -> Self {
        let one = poly.one();
        Self {
            numerator: poly,
            denominator: one,
        }
    }

    /// Return the zero rational polynomial in `n_vars` variables.
    pub fn zero(domain: &D, n_vars: usize) -> Self {
        let z = SparseMultivariatePolynomial::new(domain.clone(), n_vars);
        let one = z.one();
        Self {
            numerator: z.clone(),
            denominator: one,
        }
    }

    /// Return the unit rational polynomial (1/1) in `n_vars` variables.
    pub fn one(domain: &D, n_vars: usize) -> Self {
        let o = SparseMultivariatePolynomial::new(domain.clone(), n_vars).one();
        Self {
            numerator: o.clone(),
            denominator: o,
        }
    }

    /// Return whether this is the zero rational polynomial.
    pub fn is_zero(&self) -> bool {
        self.numerator.is_zero()
    }

    /// Return whether this is the unit rational polynomial (1/1).
    pub fn is_one(&self) -> bool {
        self.numerator == self.denominator
    }

    /// Return the number of variables.
    pub fn n_vars(&self) -> usize {
        self.numerator.n_vars()
    }

    /// Return a reference to the coefficient domain.
    pub fn domain(&self) -> &D {
        self.numerator.domain()
    }

    /// Return the negation: $-\frac{n}{d}$.
    pub fn neg(&self) -> Self {
        Self {
            numerator: self.numerator.neg(),
            denominator: self.denominator.clone(),
        }
    }

    /// Return the multiplicative inverse: $\frac{d}{n}$.
    ///
    /// Returns `None` if the numerator is zero.
    pub fn inv(&self) -> Option<Self> {
        if self.numerator.is_zero() {
            return None;
        }
        Some(Self {
            numerator: self.denominator.clone(),
            denominator: self.numerator.clone(),
        })
    }

    /// Return the power $\left(\frac{n}{d}\right)^k$.
    pub fn pow(&self, k: u32) -> Self {
        if k == 0 {
            return Self::one(self.domain(), self.n_vars());
        }
        // Simple repeated squaring on numerator and denominator separately.
        let mut num = self.numerator.one();
        let mut den = self.denominator.one();
        let mut base_num = self.numerator.clone();
        let mut base_den = self.denominator.clone();
        let mut exp = k;
        while exp > 0 {
            if exp & 1 == 1 {
                num = num.mul(&base_num);
                den = den.mul(&base_den);
            }
            base_num = base_num.mul(&base_num);
            base_den = base_den.mul(&base_den);
            exp >>= 1;
        }
        Self {
            numerator: num,
            denominator: den,
        }
    }
}

impl<D: EuclideanDomain, O: MonomialOrder> RationalPolynomial<D, O> {
    // ------------------------------------------------------------------
    //  Canonicalized constructors
    // ------------------------------------------------------------------

    /// Create a canonicalized rational polynomial from numerator and
    /// denominator.
    ///
    /// The result has coprime numerator and denominator, with the
    /// denominator's leading coefficient normalized.
    pub fn from_num_den(
        numerator: SparseMultivariatePolynomial<D, O>,
        denominator: SparseMultivariatePolynomial<D, O>,
    ) -> Self {
        if denominator.is_zero() {
            panic!("RationalPolynomial::from_num_den: denominator is zero");
        }
        if numerator.is_zero() {
            return Self {
                numerator,
                denominator,
            };
        }
        let mut rat = Self {
            numerator,
            denominator,
        };
        rat.canonicalize();
        rat
    }

    /// Reduce the fraction to canonical form.
    ///
    /// 1. Compute $\gcd(\text{num}, \text{den})$ and divide both.
    /// 2. Normalize the denominator's leading coefficient.
    fn canonicalize(&mut self) {
        if self.numerator.is_zero() {
            return;
        }
        // Step 1: GCD reduction.
        // Use the multivariate GCD infrastructure. For bivariate polynomials
        // we use bivariate_gcd; for general case we use gcd_modular.
        // Both operate on IntegerDomain/Lex, so we need to handle the generic
        // case differently. For now, use a simple approach: compute content
        // GCD and primitive parts.
        let num_content = self.numerator.content();
        let den_content = self.denominator.content();
        let coeff_gcd = self.numerator.domain().gcd(&num_content, &den_content);

        if !self.numerator.domain().is_one(&coeff_gcd) {
            self.numerator = self.numerator.div_scalar(&coeff_gcd);
            self.denominator = self.denominator.div_scalar(&coeff_gcd);
        }

        // Step 2: Normalize leading coefficient of denominator.
        // For IntegerDomain/RationalDomain: ensure positive leading coefficient.
        // For FiniteField: ensure leading coefficient is 1.
        if let Some(den_lc) = self.denominator.leading_coeff() {
            // If leading coefficient is "negative" (check via domain), negate both.
            // For domains without ordering, we just ensure consistency.
            if let Some(neg_lc) = self.numerator.domain().inv(den_lc) {
                // FiniteField or field: divide both by leading coeff to make den monic.
                self.numerator = self.numerator.mul_scalar(&neg_lc);
                self.denominator = self.denominator.mul_scalar(&neg_lc);
            }
        }
    }

    // ------------------------------------------------------------------
    //  Arithmetic (EuclideanDomain required for GCD-based operations)
    // ------------------------------------------------------------------

    /// Add two rational polynomials: $\frac{a}{b} + \frac{c}{d}$.
    ///
    /// Uses the denominator-GCD strategy to minimize intermediate growth.
    pub fn add(&self, other: &Self) -> Self {
        if self.is_zero() {
            return other.clone();
        }
        if other.is_zero() {
            return self.clone();
        }

        // Check for same denominator (common case).
        if self.denominator == other.denominator {
            let num = self.numerator.add(&other.numerator);
            return Self::from_num_den(num, self.denominator.clone());
        }

        // General case: cross-multiply then canonicalize.
        // TODO: optimize with denominator GCD strategy (Symbolica-style).
        let ad = self.numerator.mul(&other.denominator);
        let bc = other.numerator.mul(&self.denominator);
        let num = ad.add(&bc);
        let den = self.denominator.mul(&other.denominator);
        Self::from_num_den(num, den)
    }

    /// Subtract two rational polynomials.
    pub fn sub(&self, other: &Self) -> Self {
        self.add(&other.neg())
    }

    /// Multiply two rational polynomials with cross-cancellation.
    ///
    /// Computes $\gcd(a, d)$ and $\gcd(b, c)$ before multiplying to
    /// reduce intermediate coefficient growth.
    pub fn mul(&self, other: &Self) -> Self {
        if self.is_zero() || other.is_zero() {
            return Self::zero(self.domain(), self.n_vars());
        }

        // Cross-cancellation: gcd(num1, den2) and gcd(den1, num2).
        // For generic domains, fall back to simple multiply + canonicalize.
        // TODO: implement cross-cancellation with multivariate GCD.
        let num = self.numerator.mul(&other.numerator);
        let den = self.denominator.mul(&other.denominator);
        Self::from_num_den(num, den)
    }

    /// Divide two rational polynomials: $\frac{a/b}{c/d} = \frac{ad}{bc}$.
    pub fn div(&self, other: &Self) -> Option<Self> {
        let inv = other.inv()?;
        Some(self.mul(&inv))
    }
}

// ------------------------------------------------------------------
//  Display
// ------------------------------------------------------------------

impl<D: Domain, O: MonomialOrder> fmt::Display for RationalPolynomial<D, O>
where
    D::Element: fmt::Display,
{
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if self.denominator.is_zero() || self.denominator.n_terms() <= 1 {
            // Check if denominator is constant 1.
            let const_val = self.denominator.coeff(&vec![0; self.denominator.n_vars()]);
            if self.domain().is_one(&const_val) {
                return write!(f, "{:?}", self.numerator);
            }
        }
        write!(f, "({:?}) / ({:?})", self.numerator, self.denominator)
    }
}

// ------------------------------------------------------------------
//  Helper: div_scalar on SparseMultivariatePolynomial
// ------------------------------------------------------------------

impl<D: EuclideanDomain, O: MonomialOrder> SparseMultivariatePolynomial<D, O> {
    /// Divide all coefficients by a scalar (must divide exactly).
    fn div_scalar(&self, scalar: &D::Element) -> Self {
        if self.domain().is_one(scalar) {
            return self.clone();
        }
        let inv = self
            .domain()
            .inv(scalar)
            .expect("div_scalar: cannot invert zero");
        self.mul_scalar(&inv)
    }
}

// ------------------------------------------------------------------
//  Tests
// ------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use crate::sparse::Lex;
    use ocas_domain::{Integer, IntegerDomain};

    type ZPoly = SparseMultivariatePolynomial<IntegerDomain, Lex>;
    type ZRat = RationalPolynomial<IntegerDomain, Lex>;

    fn poly1(terms: Vec<(Vec<usize>, i64)>) -> ZPoly {
        ZPoly::from_terms(
            IntegerDomain,
            1,
            terms
                .into_iter()
                .map(|(e, c)| (e, Integer::from(c)))
                .collect(),
        )
    }

    #[allow(dead_code)]
    fn poly2(terms: Vec<(Vec<usize>, i64)>, n_vars: usize) -> ZPoly {
        ZPoly::from_terms(
            IntegerDomain,
            n_vars,
            terms
                .into_iter()
                .map(|(e, c)| (e, Integer::from(c)))
                .collect(),
        )
    }

    #[test]
    fn rational_zero_and_one() {
        let z = ZRat::zero(&IntegerDomain, 1);
        assert!(z.is_zero());
        assert!(!z.is_one());

        let o = ZRat::one(&IntegerDomain, 1);
        assert!(!o.is_zero());
        assert!(o.is_one());
    }

    #[test]
    fn rational_from_polynomial() {
        // p = x + 1
        let p = poly1(vec![(vec![0], 1), (vec![1], 1)]);
        let r = ZRat::from_polynomial(p.clone());
        assert_eq!(r.numerator, p);
        assert!(r.denominator.n_terms() <= 1);
    }

    #[test]
    fn rational_neg() {
        // r = x / (x + 1)
        let num = poly1(vec![(vec![1], 1)]);
        let den = poly1(vec![(vec![0], 1), (vec![1], 1)]);
        let r = ZRat::new(num, den);
        let nr = r.neg();
        // -x / (x+1)
        assert_eq!(nr.numerator.coeff(&[1]), Integer::from(-1));
    }

    #[test]
    fn rational_add_same_den() {
        // 1/x + 1/x = 2/x
        let x = poly1(vec![(vec![1], 1)]);
        let one = poly1(vec![(vec![0], 1)]);

        let r1 = ZRat::new(one.clone(), x.clone());
        let r2 = ZRat::new(one, x.clone());
        let sum = r1.add(&r2);

        // After canonicalization: 2/x
        assert_eq!(sum.numerator.coeff(&[0]), Integer::from(2));
    }

    #[test]
    fn rational_add_different_den() {
        // 1/(x-1) + 1/(x+1) = 2x / (x^2-1)
        // x-1 = [-1, 1]  (constant -1, coeff of x is 1)
        let x_minus_1 = poly1(vec![(vec![0], -1), (vec![1], 1)]);
        let x_plus_1 = poly1(vec![(vec![0], 1), (vec![1], 1)]);
        let one = poly1(vec![(vec![0], 1)]);

        let r1 = ZRat::new(one.clone(), x_minus_1);
        let r2 = ZRat::new(one, x_plus_1);
        let sum = r1.add(&r2);

        // Result should be non-zero.
        assert!(!sum.is_zero());
        // Verify by multiplying back: sum * den should equal num.
    }

    #[test]
    fn rational_mul() {
        // (x+1)/(x-1) * (x-1)/(x+1) = 1
        let x_plus_1 = poly1(vec![(vec![0], 1), (vec![1], 1)]);
        let x_minus_1 = poly1(vec![(vec![0], -1), (vec![1], 1)]);

        let r1 = ZRat::new(x_plus_1.clone(), x_minus_1.clone());
        let r2 = ZRat::new(x_minus_1, x_plus_1);
        let prod = r1.mul(&r2);

        // Should canonicalize to 1/1.
        assert!(prod.is_one() || (prod.numerator == prod.denominator));
    }

    #[test]
    fn rational_inv() {
        let x = poly1(vec![(vec![1], 1)]);
        let one = poly1(vec![(vec![0], 1)]);
        let r = ZRat::new(x, one);
        let r_inv = r.inv().unwrap();

        // inv(x/1) = 1/x
        assert_eq!(r_inv.numerator, r_inv.denominator.one());
    }

    #[test]
    fn rational_pow() {
        // (x/1)^3 = x^3/1
        let x = poly1(vec![(vec![1], 1)]);
        let one = poly1(vec![(vec![0], 1)]);
        let r = ZRat::new(x, one);
        let r3 = r.pow(3);

        // numerator should be x^3
        assert_eq!(r3.numerator.coeff(&[3]), Integer::from(1));
        assert_eq!(r3.numerator.n_terms(), 1);
    }
}