oxiz_math/
rational_function.rs

1//! Rational function arithmetic.
2//!
3//! This module provides operations on rational functions, which are quotients
4//! of polynomials: f(x) = p(x) / q(x).
5//!
6//! Rational functions are useful for:
7//! - Symbolic computation
8//! - Partial fraction decomposition
9//! - Control theory and signal processing
10//! - Approximation theory
11
12use crate::polynomial::{Polynomial, Var};
13use num_rational::BigRational;
14use num_traits::Zero;
15use std::ops::{Add, Div, Mul, Neg, Sub};
16
17/// A rational function represented as numerator / denominator.
18///
19/// Invariants:
20/// - The denominator is never the zero polynomial
21/// - The representation is kept in reduced form (gcd of numerator and denominator is 1)
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct RationalFunction {
24    numerator: Polynomial,
25    denominator: Polynomial,
26}
27
28impl RationalFunction {
29    /// Create a new rational function from numerator and denominator.
30    ///
31    /// # Arguments
32    ///
33    /// * `numerator` - The numerator polynomial
34    /// * `denominator` - The denominator polynomial
35    ///
36    /// # Panics
37    ///
38    /// Panics if the denominator is the zero polynomial.
39    ///
40    /// # Examples
41    ///
42    /// ```
43    /// use oxiz_math::polynomial::Polynomial;
44    /// use oxiz_math::rational_function::RationalFunction;
45    ///
46    /// let num = Polynomial::from_coeffs_int(&[(1, &[(0, 1)])]); // x
47    /// let den = Polynomial::from_coeffs_int(&[(1, &[(0, 1)]), (1, &[])]); // x + 1
48    /// let rf = RationalFunction::new(num, den);
49    /// ```
50    pub fn new(numerator: Polynomial, denominator: Polynomial) -> Self {
51        assert!(!denominator.is_zero(), "Denominator cannot be zero");
52
53        let mut rf = RationalFunction {
54            numerator,
55            denominator,
56        };
57        rf.reduce();
58        rf
59    }
60
61    /// Create a rational function from a polynomial (denominator = 1).
62    ///
63    /// # Examples
64    ///
65    /// ```
66    /// use oxiz_math::polynomial::Polynomial;
67    /// use oxiz_math::rational_function::RationalFunction;
68    ///
69    /// let p = Polynomial::from_coeffs_int(&[(1, &[(0, 2)])]); // x^2
70    /// let rf = RationalFunction::from_polynomial(p);
71    /// ```
72    pub fn from_polynomial(p: Polynomial) -> Self {
73        RationalFunction {
74            numerator: p,
75            denominator: Polynomial::one(),
76        }
77    }
78
79    /// Create a rational function from a constant.
80    ///
81    /// # Examples
82    ///
83    /// ```
84    /// use num_bigint::BigInt;
85    /// use num_rational::BigRational;
86    /// use oxiz_math::rational_function::RationalFunction;
87    ///
88    /// let rf = RationalFunction::from_constant(BigRational::from_integer(BigInt::from(5)));
89    /// ```
90    pub fn from_constant(c: BigRational) -> Self {
91        RationalFunction {
92            numerator: Polynomial::constant(c),
93            denominator: Polynomial::one(),
94        }
95    }
96
97    /// Get the numerator.
98    pub fn numerator(&self) -> &Polynomial {
99        &self.numerator
100    }
101
102    /// Get the denominator.
103    pub fn denominator(&self) -> &Polynomial {
104        &self.denominator
105    }
106
107    /// Check if the rational function is zero.
108    pub fn is_zero(&self) -> bool {
109        self.numerator.is_zero()
110    }
111
112    /// Check if the rational function is constant.
113    pub fn is_constant(&self) -> bool {
114        self.numerator.is_constant() && self.denominator.is_constant()
115    }
116
117    /// Reduce the rational function to lowest terms.
118    ///
119    /// Divides both numerator and denominator by their GCD.
120    fn reduce(&mut self) {
121        if self.numerator.is_zero() {
122            self.denominator = Polynomial::one();
123            return;
124        }
125
126        // For univariate case, compute GCD and use pseudo_div to reduce
127        if self.numerator.is_univariate() && self.denominator.is_univariate() {
128            let gcd = self.numerator.gcd_univariate(&self.denominator);
129            if !gcd.is_constant() {
130                // Use pseudo_div_univariate to divide both by GCD
131                let (q1, r1) = self.numerator.pseudo_div_univariate(&gcd);
132                let (q2, r2) = self.denominator.pseudo_div_univariate(&gcd);
133
134                // If remainders are zero, we can reduce
135                if r1.is_zero() && r2.is_zero() {
136                    self.numerator = q1;
137                    self.denominator = q2;
138                }
139            }
140        }
141        // For multivariate or when GCD reduction fails, leave as is
142    }
143
144    /// Evaluate the rational function at a point.
145    ///
146    /// # Arguments
147    ///
148    /// * `assignment` - Variable assignments
149    ///
150    /// # Returns
151    ///
152    /// The value of the rational function, or None if the denominator evaluates to zero.
153    ///
154    /// # Examples
155    ///
156    /// ```
157    /// use num_bigint::BigInt;
158    /// use num_rational::BigRational;
159    /// use oxiz_math::polynomial::Polynomial;
160    /// use oxiz_math::rational_function::RationalFunction;
161    /// use rustc_hash::FxHashMap;
162    ///
163    /// let num = Polynomial::from_coeffs_int(&[(1, &[(0, 1)])]); // x
164    /// let den = Polynomial::from_coeffs_int(&[(1, &[(0, 1)]), (1, &[])]); // x + 1
165    /// let rf = RationalFunction::new(num, den);
166    ///
167    /// let mut assignment = FxHashMap::default();
168    /// assignment.insert(0, BigRational::from_integer(BigInt::from(2)));
169    /// let val = rf.eval(&assignment).unwrap();
170    /// assert_eq!(val, BigRational::new(BigInt::from(2), BigInt::from(3))); // 2/3
171    /// ```
172    pub fn eval(
173        &self,
174        assignment: &rustc_hash::FxHashMap<Var, BigRational>,
175    ) -> Option<BigRational> {
176        let num_val = self.numerator.eval(assignment);
177        let den_val = self.denominator.eval(assignment);
178
179        if den_val.is_zero() {
180            None
181        } else {
182            Some(num_val / den_val)
183        }
184    }
185
186    /// Compute the derivative of the rational function.
187    ///
188    /// Uses the quotient rule: (p/q)' = (p'q - pq') / q^2
189    ///
190    /// # Examples
191    ///
192    /// ```
193    /// use oxiz_math::polynomial::Polynomial;
194    /// use oxiz_math::rational_function::RationalFunction;
195    ///
196    /// let num = Polynomial::from_coeffs_int(&[(1, &[(0, 1)])]); // x
197    /// let den = Polynomial::from_coeffs_int(&[(1, &[(0, 1)]), (1, &[])]); // x + 1
198    /// let rf = RationalFunction::new(num, den);
199    /// let derivative = rf.derivative(0);
200    /// ```
201    pub fn derivative(&self, var: Var) -> RationalFunction {
202        // (p/q)' = (p'q - pq') / q^2
203        let p_prime = self.numerator.derivative(var);
204        let q_prime = self.denominator.derivative(var);
205
206        let numerator = p_prime * self.denominator.clone() - self.numerator.clone() * q_prime;
207        let denominator = self.denominator.clone() * self.denominator.clone();
208
209        RationalFunction::new(numerator, denominator)
210    }
211
212    /// Simplify the rational function.
213    ///
214    /// This is an alias for reducing to lowest terms.
215    pub fn simplify(&mut self) {
216        self.reduce();
217    }
218
219    /// Check if this is a proper rational function (degree of numerator < degree of denominator).
220    ///
221    /// # Examples
222    ///
223    /// ```
224    /// use oxiz_math::polynomial::Polynomial;
225    /// use oxiz_math::rational_function::RationalFunction;
226    ///
227    /// let num = Polynomial::from_coeffs_int(&[(1, &[(0, 1)])]); // x
228    /// let den = Polynomial::from_coeffs_int(&[(1, &[(0, 2)])]); // x^2
229    /// let rf = RationalFunction::new(num, den);
230    /// assert!(rf.is_proper());
231    /// ```
232    pub fn is_proper(&self) -> bool {
233        self.numerator.total_degree() < self.denominator.total_degree()
234    }
235
236    /// Perform polynomial long division to separate improper rational functions.
237    ///
238    /// For an improper rational function N(x)/D(x) where deg(N) >= deg(D),
239    /// returns (Q(x), R(x)/D(x)) where N(x)/D(x) = Q(x) + R(x)/D(x)
240    /// and R(x)/D(x) is proper.
241    ///
242    /// # Arguments
243    ///
244    /// * `var` - The variable for univariate division
245    ///
246    /// # Returns
247    ///
248    /// * `(quotient_polynomial, proper_remainder)` if the function is improper and univariate
249    /// * `None` if the function is proper or not univariate
250    ///
251    /// # Examples
252    ///
253    /// ```
254    /// use oxiz_math::polynomial::Polynomial;
255    /// use oxiz_math::rational_function::RationalFunction;
256    ///
257    /// // (x^2 + 1) / x = x + 1/x
258    /// let num = Polynomial::from_coeffs_int(&[(1, &[(0, 2)]), (1, &[])]);
259    /// let den = Polynomial::from_coeffs_int(&[(1, &[(0, 1)])]);
260    /// let rf = RationalFunction::new(num, den);
261    ///
262    /// let (quotient, remainder) = rf.polynomial_division(0).unwrap();
263    /// // quotient should be x, remainder should be 1/x
264    /// ```
265    pub fn polynomial_division(&self, _var: Var) -> Option<(Polynomial, RationalFunction)> {
266        // Only works for univariate and improper fractions
267        if !self.numerator.is_univariate() || !self.denominator.is_univariate() {
268            return None;
269        }
270
271        if self.is_proper() {
272            return None;
273        }
274
275        // Perform polynomial division
276        let (quotient, remainder) = self.numerator.pseudo_div_univariate(&self.denominator);
277
278        // Create the proper fraction from the remainder
279        let proper_fraction = RationalFunction::new(remainder, self.denominator.clone());
280
281        Some((quotient, proper_fraction))
282    }
283
284    /// Compute partial fraction decomposition for simple cases.
285    ///
286    /// For a proper rational function with a factored denominator of distinct linear factors,
287    /// decomposes into a sum of simpler fractions.
288    ///
289    /// **Note:** This is a simplified implementation that works for:
290    /// - Proper rational functions (deg(numerator) < deg(denominator))
291    /// - Univariate polynomials
292    /// - Denominators that are products of distinct linear factors
293    ///
294    /// For more complex cases (repeated factors, irreducible quadratics),
295    /// use specialized computer algebra systems.
296    ///
297    /// # Arguments
298    ///
299    /// * `var` - The variable
300    ///
301    /// # Returns
302    ///
303    /// Vector of simpler rational functions that sum to the original,
304    /// or None if decomposition is not applicable
305    ///
306    /// # Examples
307    ///
308    /// ```
309    /// use oxiz_math::polynomial::Polynomial;
310    /// use oxiz_math::rational_function::RationalFunction;
311    ///
312    /// // Example: 1 / (x(x-1)) = A/x + B/(x-1)
313    /// // where A = -1, B = 1
314    /// let num = Polynomial::from_coeffs_int(&[(1, &[])]);  // 1
315    /// let den = Polynomial::from_coeffs_int(&[             // x^2 - x
316    ///     (1, &[(0, 2)]),   // x^2
317    ///     (-1, &[(0, 1)]),  // -x
318    /// ]);
319    /// let rf = RationalFunction::new(num, den);
320    ///
321    /// // Partial fraction decomposition (if factors are available)
322    /// // This is a placeholder - full implementation would require factorization
323    /// ```
324    pub fn partial_fraction_decomposition(&self, _var: Var) -> Option<Vec<RationalFunction>> {
325        // Check if proper
326        if !self.is_proper() {
327            return None;
328        }
329
330        // Check if univariate
331        if !self.numerator.is_univariate() || !self.denominator.is_univariate() {
332            return None;
333        }
334
335        // For now, return None as a full implementation would require:
336        // 1. Factorization of the denominator into irreducible factors
337        // 2. Solving systems of equations for unknown coefficients
338        // 3. Handling repeated factors and irreducible quadratics
339        //
340        // This is left as a future enhancement when polynomial factorization
341        // over rationals is more complete in the polynomial module.
342        None
343    }
344}
345
346impl Add for RationalFunction {
347    type Output = RationalFunction;
348
349    /// Add two rational functions: p/q + r/s = (ps + qr) / (qs)
350    fn add(self, other: RationalFunction) -> RationalFunction {
351        let numerator = self.numerator.clone() * other.denominator.clone()
352            + other.numerator.clone() * self.denominator.clone();
353        let denominator = self.denominator * other.denominator;
354        RationalFunction::new(numerator, denominator)
355    }
356}
357
358impl Add for &RationalFunction {
359    type Output = RationalFunction;
360
361    fn add(self, other: &RationalFunction) -> RationalFunction {
362        let numerator = self.numerator.clone() * other.denominator.clone()
363            + other.numerator.clone() * self.denominator.clone();
364        let denominator = self.denominator.clone() * other.denominator.clone();
365        RationalFunction::new(numerator, denominator)
366    }
367}
368
369impl Sub for RationalFunction {
370    type Output = RationalFunction;
371
372    /// Subtract two rational functions: p/q - r/s = (ps - qr) / (qs)
373    fn sub(self, other: RationalFunction) -> RationalFunction {
374        let numerator = self.numerator.clone() * other.denominator.clone()
375            - other.numerator.clone() * self.denominator.clone();
376        let denominator = self.denominator * other.denominator;
377        RationalFunction::new(numerator, denominator)
378    }
379}
380
381impl Sub for &RationalFunction {
382    type Output = RationalFunction;
383
384    fn sub(self, other: &RationalFunction) -> RationalFunction {
385        let numerator = self.numerator.clone() * other.denominator.clone()
386            - other.numerator.clone() * self.denominator.clone();
387        let denominator = self.denominator.clone() * other.denominator.clone();
388        RationalFunction::new(numerator, denominator)
389    }
390}
391
392impl Mul for RationalFunction {
393    type Output = RationalFunction;
394
395    /// Multiply two rational functions: (p/q) * (r/s) = (pr) / (qs)
396    fn mul(self, other: RationalFunction) -> RationalFunction {
397        let numerator = self.numerator * other.numerator;
398        let denominator = self.denominator * other.denominator;
399        RationalFunction::new(numerator, denominator)
400    }
401}
402
403impl Mul for &RationalFunction {
404    type Output = RationalFunction;
405
406    fn mul(self, other: &RationalFunction) -> RationalFunction {
407        let numerator = self.numerator.clone() * other.numerator.clone();
408        let denominator = self.denominator.clone() * other.denominator.clone();
409        RationalFunction::new(numerator, denominator)
410    }
411}
412
413impl Div for RationalFunction {
414    type Output = RationalFunction;
415
416    /// Divide two rational functions: (p/q) / (r/s) = (ps) / (qr)
417    fn div(self, other: RationalFunction) -> RationalFunction {
418        let numerator = self.numerator * other.denominator;
419        let denominator = self.denominator * other.numerator;
420        RationalFunction::new(numerator, denominator)
421    }
422}
423
424impl Div for &RationalFunction {
425    type Output = RationalFunction;
426
427    fn div(self, other: &RationalFunction) -> RationalFunction {
428        let numerator = self.numerator.clone() * other.denominator.clone();
429        let denominator = self.denominator.clone() * other.numerator.clone();
430        RationalFunction::new(numerator, denominator)
431    }
432}
433
434impl Neg for RationalFunction {
435    type Output = RationalFunction;
436
437    /// Negate a rational function: -(p/q) = (-p)/q
438    fn neg(self) -> RationalFunction {
439        RationalFunction {
440            numerator: -self.numerator,
441            denominator: self.denominator,
442        }
443    }
444}
445
446impl Neg for &RationalFunction {
447    type Output = RationalFunction;
448
449    fn neg(self) -> RationalFunction {
450        RationalFunction {
451            numerator: -self.numerator.clone(),
452            denominator: self.denominator.clone(),
453        }
454    }
455}
456
457#[cfg(test)]
458mod tests {
459    use super::*;
460    use num_bigint::BigInt;
461
462    fn rat(n: i64) -> BigRational {
463        BigRational::from_integer(BigInt::from(n))
464    }
465
466    #[test]
467    fn test_rational_function_creation() {
468        let num = Polynomial::from_coeffs_int(&[(1, &[(0, 1)])]); // x
469        let den = Polynomial::from_coeffs_int(&[(1, &[])]); // 1
470        let rf = RationalFunction::new(num, den);
471        assert!(!rf.is_zero());
472    }
473
474    #[test]
475    #[should_panic(expected = "Denominator cannot be zero")]
476    fn test_rational_function_zero_denominator() {
477        let num = Polynomial::from_coeffs_int(&[(1, &[(0, 1)])]); // x
478        let den = Polynomial::zero();
479        let _rf = RationalFunction::new(num, den);
480    }
481
482    #[test]
483    fn test_rational_function_addition() {
484        // x/1 + 1/1 = (x + 1)/1
485        let rf1 = RationalFunction::from_polynomial(
486            Polynomial::from_coeffs_int(&[(1, &[(0, 1)])]), // x
487        );
488        let rf2 = RationalFunction::from_constant(rat(1));
489
490        let sum = rf1 + rf2;
491        let mut assignment = rustc_hash::FxHashMap::default();
492        assignment.insert(0, rat(2));
493        assert_eq!(sum.eval(&assignment).unwrap(), rat(3)); // 2 + 1 = 3
494    }
495
496    #[test]
497    fn test_rational_function_multiplication() {
498        // (x/1) * (2/1) = (2x)/1
499        let rf1 = RationalFunction::from_polynomial(
500            Polynomial::from_coeffs_int(&[(1, &[(0, 1)])]), // x
501        );
502        let rf2 = RationalFunction::from_constant(rat(2));
503
504        let prod = rf1 * rf2;
505        let mut assignment = rustc_hash::FxHashMap::default();
506        assignment.insert(0, rat(3));
507        assert_eq!(prod.eval(&assignment).unwrap(), rat(6)); // 2 * 3 = 6
508    }
509
510    #[test]
511    fn test_rational_function_division() {
512        // (x) / (x+1)
513        let num = Polynomial::from_coeffs_int(&[(1, &[(0, 1)])]); // x
514        let den = Polynomial::from_coeffs_int(&[(1, &[(0, 1)]), (1, &[])]); // x + 1
515
516        let rf = RationalFunction::new(num, den.clone());
517
518        let mut assignment = rustc_hash::FxHashMap::default();
519        assignment.insert(0, rat(2));
520        assert_eq!(
521            rf.eval(&assignment).unwrap(),
522            BigRational::new(BigInt::from(2), BigInt::from(3))
523        );
524    }
525
526    #[test]
527    fn test_rational_function_derivative() {
528        // d/dx(x/(x+1)) = 1/(x+1)^2
529        let num = Polynomial::from_coeffs_int(&[(1, &[(0, 1)])]); // x
530        let den = Polynomial::from_coeffs_int(&[(1, &[(0, 1)]), (1, &[])]); // x + 1
531
532        let rf = RationalFunction::new(num, den);
533        let deriv = rf.derivative(0);
534
535        let mut assignment = rustc_hash::FxHashMap::default();
536        assignment.insert(0, rat(1));
537        // At x=1: derivative = 1/(1+1)^2 = 1/4
538        assert_eq!(
539            deriv.eval(&assignment).unwrap(),
540            BigRational::new(BigInt::from(1), BigInt::from(4))
541        );
542    }
543
544    #[test]
545    fn test_rational_function_reduction() {
546        // (x^2 - 1) / (x - 1) should reduce to (x + 1) / 1
547        let num = Polynomial::from_coeffs_int(&[
548            (1, &[(0, 2)]), // x^2
549            (-1, &[]),      // -1
550        ]);
551        let den = Polynomial::from_coeffs_int(&[
552            (1, &[(0, 1)]), // x
553            (-1, &[]),      // -1
554        ]);
555
556        let rf = RationalFunction::new(num, den);
557
558        // Should be reduced
559        let mut assignment = rustc_hash::FxHashMap::default();
560        assignment.insert(0, rat(2));
561        assert_eq!(rf.eval(&assignment).unwrap(), rat(3)); // (x + 1) at x=2 is 3
562    }
563
564    #[test]
565    fn test_rational_function_is_constant() {
566        let rf = RationalFunction::from_constant(rat(5));
567        assert!(rf.is_constant());
568
569        let rf2 = RationalFunction::from_polynomial(
570            Polynomial::from_coeffs_int(&[(1, &[(0, 1)])]), // x
571        );
572        assert!(!rf2.is_constant());
573    }
574
575    #[test]
576    fn test_rational_function_negation() {
577        let rf = RationalFunction::from_constant(rat(5));
578        let neg_rf = -rf;
579
580        let assignment = rustc_hash::FxHashMap::default();
581        assert_eq!(neg_rf.eval(&assignment).unwrap(), rat(-5));
582    }
583}