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}