Skip to main content

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