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}