From f9cbb79e902dd6e648e1ae6e7a115141b0081632 Mon Sep 17 00:00:00 2001 From: Christofer Nolander Date: Wed, 2 Mar 2022 15:33:30 +0100 Subject: [PATCH 1/3] add polynomial division resolves outstanding TODO. It turns out to be quite hard to actually define division of polynomials containing more than one variable in a satisfying manner. It is possible to defining a basis to use (for example, by sorting monomials in a lexicographic order) but this will require further work. --- algorithms/maths/polynomial.py | 63 +++++- tests/test_polynomial.py | 349 +++++++++++++++++---------------- 2 files changed, 237 insertions(+), 175 deletions(-) diff --git a/algorithms/maths/polynomial.py b/algorithms/maths/polynomial.py index 55b59dd91..35ff2f227 100644 --- a/algorithms/maths/polynomial.py +++ b/algorithms/maths/polynomial.py @@ -283,6 +283,12 @@ def __str__(self) -> str: result += temp return result + ')' + def degree(self): + """ + Get the degree of the monomial (sum of the exponents) + """ + return sum(self.variables.values()) + class Polynomial: """ @@ -450,19 +456,56 @@ def __truediv__(self, other: Union[int, float, Fraction, Monomial]): poly_temp = reduce(lambda acc, val: acc + val, map(lambda x: x / other, [z for z in self.all_monomials()]), Polynomial([Monomial({}, 0)])) return poly_temp elif isinstance(other, Polynomial): - if Monomial({}, 0) in other.all_monomials(): - if len(other.all_monomials()) == 2: - temp_set = {x for x in other.all_monomials() if x != Monomial({}, 0)} - only = temp_set.pop() - return self.__truediv__(only) - elif len(other.all_monomials()) == 1: - temp_set = {x for x in other.all_monomials()} - only = temp_set.pop() - return self.__truediv__(only) + if len(other.all_monomials()) == 1: + monomial = other.all_monomials().pop() + return self.__truediv__(monomial) + quotient, remainder = self.polynomial_division(other) + return quotient raise ValueError('Can only divide a polynomial by an int, float, Fraction, or a Monomial.') + + def polynomial_division(self, other): + """ + Perform polynomial division. + """ + variables = self.variables() | other.variables() + if len(variables) > 1: + # Polynomial division when there's more than one variable turns out + # to be hard to define, as the answer depends on the order + # of the monomials. + # Reference: https://math.stackexchange.com/questions/32070/what-is-the-algorithm-for-long-division-of-polynomials-with-multiple-variables + raise ValueError("cannot divide polynomials containing more than one variable") + + # Implementation using standard long division + # Reference: https://en.wikipedia.org/wiki/Polynomial_long_division + remainder = self + quotient = Polynomial([]) + while len(remainder.all_monomials()) > 0: + # Find the monomials with highest degree + remainder_max = max(remainder.all_monomials(), key=lambda m: m.degree()) + other_max = max(other.all_monomials(), key=lambda m: m.degree()) + + # Continue until the remainder's degree cannot be reduced further + if remainder_max.degree() < other_max.degree(): + break + + # Find the factor required to reduce the remainders degree by one (or more) + factor = remainder_max / other_max + remainder -= other * factor + quotient += factor + + return quotient, remainder + + @staticmethod + def check_only_one_variable(monomial: Monomial) -> bool: + count = 0 + for variable, degree in monomial.variables.items(): + if degree == 0: + continue + count += 1 - return + # Can either contain zero or one variables + return count <= 1 # def clone(self) -> Polynomial: def clone(self): diff --git a/tests/test_polynomial.py b/tests/test_polynomial.py index 4ba2ba6b7..0ea0c61c1 100644 --- a/tests/test_polynomial.py +++ b/tests/test_polynomial.py @@ -1,6 +1,6 @@ from algorithms.maths.polynomial import ( - Polynomial, - Monomial + Polynomial, + Monomial ) from fractions import Fraction import math @@ -10,166 +10,185 @@ class TestSuite(unittest.TestCase): - def setUp(self): - self.p0 = Polynomial([ - Monomial({}) - ]) - self.p1 = Polynomial([ - Monomial({}), Monomial({}) - ]) - self.p2 = Polynomial([ - Monomial({1: 1}, 2) - ]) - self.p3 = Polynomial([ - Monomial({1: 1}, 2), - Monomial({1: 2, 2: -1}, 1.5) - ]) - self.p4 = Polynomial([ - Monomial({2: 1, 3: 0}, Fraction(2, 3)), - Monomial({1: -1, 3: 2}, math.pi), - Monomial({1: -1, 3: 2}, 1) - ]) - self.p5 = Polynomial([ - Monomial({150: 5, 170: 2, 10000:3}, 0), - Monomial({1: -1, 3: 2}, 1), - ]) - self.p6 = Polynomial([ - 2, - -3, - Fraction(1, 7), - 2**math.pi, - Monomial({2: 3, 3: 1}, 1.25) - ]) - self.p7 = Polynomial([ - Monomial({1: 1}, -2), - Monomial({1: 2, 2: -1}, -1.5) - ]) - - self.m1 = Monomial({1: 2, 2: 3}, -1) - - return - - def test_polynomial_addition(self): - - # The zero polynomials should add up to - # itselves only. - self.assertEqual(self.p0 + self.p1, self.p0) - self.assertEqual(self.p0 + self.p1, self.p1) - - # Additive inverses should add up to the - # zero polynomial. - self.assertEqual(self.p3 + self.p7, self.p0) - self.assertEqual(self.p3 + self.p7, self.p1) - - # Like terms should combine. - # The order of monomials should not matter. - self.assertEqual(self.p2 + self.p3, Polynomial([ - Monomial({1: 1}, 4), - Monomial({1: 2, 2: -1}, 1.5) - ])) - self.assertEqual(self.p2 + self.p3, Polynomial([ - Monomial({1: 2, 2: -1}, 1.5), - Monomial({1: 1}, 4), - ])) - - # Another typical computation. - self.assertEqual(self.p5 + self.p6, Polynomial([ - Monomial({}, 7.96783496993343), - Monomial({2: 3, 3: 1}, 1.25), - Monomial({1: -1, 3: 2}) - ])) - - return - - def test_polynomial_subtraction(self): - - self.assertEqual(self.p3 - self.p2, Polynomial([ - Monomial({1: 2, 2: -1}, 1.5) - ])) - - self.assertEqual(self.p3 - self.p3, Polynomial([])) - - self.assertEqual(self.p2 - self.p3, Polynomial([ - Monomial({1: 2, 2: -1}, -1.5) - ])) - - pass - - def test_polynomial_multiplication(self): - self.assertEqual(self.p0 * self.p2, Polynomial([])) - self.assertEqual(self.p1 * self.p2, Polynomial([])) - - self.assertEqual(self.p2 * self.p3, Polynomial([ - Monomial({1: 2}, 4), - Monomial({1: 3, 2: -1}, Fraction(3, 1)) - ])) - return - - def test_polynomial_division(self): - - # Should raise a ValueError if the divisor is not a monomial - # or a polynomial with only one term. - self.assertRaises(ValueError, lambda x, y: x / y, self.p5, self.p3) - self.assertRaises(ValueError, lambda x, y: x / y, self.p6, self.p4) - - self.assertEqual(self.p3 / self.p2, Polynomial([ - Monomial({}, 1), - Monomial({1: 1, 2: -1}, 0.75) - ])) - self.assertEqual(self.p7 / self.m1, Polynomial([ - Monomial({1: -1, 2: -3}, 2), - Monomial({1: 0, 2: -4}, 1.5) - ])) - self.assertEqual(self.p7 / self.m1, Polynomial([ - Monomial({1: -1, 2: -3}, 2), - Monomial({2: -4}, 1.5) - ])) - return - - def test_polynomial_variables(self): - # The zero polynomial has no variables. - - self.assertEqual(self.p0.variables(), set()) - self.assertEqual(self.p1.variables(), set()) - - # The total variables are the union of the variables - # from the monomials. - self.assertEqual(self.p4.variables(), {1, 2, 3}) - - # The monomials with coefficient 0 should be dropped. - self.assertEqual(self.p5.variables(), {1, 3}) - return - - def test_polynomial_subs(self): - # Anything substitued in the zero polynomial - # should evaluate to 0. - self.assertEqual(self.p1.subs(2), 0) - self.assertEqual(self.p0.subs(-101231), 0) - - # Should raise a ValueError if not enough variables are supplied. - self.assertRaises(ValueError, lambda x, y: x.subs(y), self.p4, {1: 3, 2: 2}) - self.assertRaises(ValueError, lambda x, y: x.subs(y), self.p4, {}) - - # Should work fine if a complete subsitution map is provided. - self.assertAlmostEqual(self.p4.subs({1: 1, 2: 1, 3: 1}), (1 + math.pi + Fraction(2, 3)), delta=1e-9) - # Should work fine if more than enough substitutions are provided. - self.assertAlmostEqual(self.p4.subs({1: 1, 2: 1, 3: 1, 4: 1}), (1 + math.pi + Fraction(2, 3)), delta=1e-9) - return - - def test_polynomial_clone(self): - - # The zero polynomial always clones to itself. - self.assertEqual(self.p0.clone(), self.p0) - self.assertEqual(self.p1.clone(), self.p0) - self.assertEqual(self.p0.clone(), self.p1) - self.assertEqual(self.p1.clone(), self.p1) - - # The polynomial should clone nicely. - self.assertEqual(self.p4.clone(), self.p4) - - # The monomial with a zero coefficient should be dropped - # in the clone. - self.assertEqual(self.p5.clone(), Polynomial([ - Monomial({1: -1, 3: 2}, 1) - ])) - return \ No newline at end of file + def setUp(self): + self.p0 = Polynomial([ + Monomial({}) + ]) + self.p1 = Polynomial([ + Monomial({}), Monomial({}) + ]) + self.p2 = Polynomial([ + Monomial({1: 1}, 2) + ]) + self.p3 = Polynomial([ + Monomial({1: 1}, 2), + Monomial({1: 2, 2: -1}, 1.5) + ]) + self.p4 = Polynomial([ + Monomial({2: 1, 3: 0}, Fraction(2, 3)), + Monomial({1: -1, 3: 2}, math.pi), + Monomial({1: -1, 3: 2}, 1) + ]) + self.p5 = Polynomial([ + Monomial({150: 5, 170: 2, 10000:3}, 0), + Monomial({1: -1, 3: 2}, 1), + ]) + self.p6 = Polynomial([ + 2, + -3, + Fraction(1, 7), + 2**math.pi, + Monomial({2: 3, 3: 1}, 1.25) + ]) + self.p7 = Polynomial([ + Monomial({1: 1}, -2), + Monomial({1: 2, 2: -1}, -1.5) + ]) + + # x^3 - 2x^2 - 4 + self.p8 = Polynomial([ + Monomial({1:3}, 1), + Monomial({1:2}, -2), + -4 + ]) + # x - 3 + self.p9 = Polynomial([ + Monomial({1:1}, 1), + -3, + ]) + + self.m1 = Monomial({1: 2, 2: 3}, -1) + + def test_polynomial_addition(self): + + # The zero polynomials should add up to + # itselves only. + self.assertEqual(self.p0 + self.p1, self.p0) + self.assertEqual(self.p0 + self.p1, self.p1) + + # Additive inverses should add up to the + # zero polynomial. + self.assertEqual(self.p3 + self.p7, self.p0) + self.assertEqual(self.p3 + self.p7, self.p1) + + # Like terms should combine. + # The order of monomials should not matter. + self.assertEqual(self.p2 + self.p3, Polynomial([ + Monomial({1: 1}, 4), + Monomial({1: 2, 2: -1}, 1.5) + ])) + self.assertEqual(self.p2 + self.p3, Polynomial([ + Monomial({1: 2, 2: -1}, 1.5), + Monomial({1: 1}, 4), + ])) + + # Another typical computation. + self.assertEqual(self.p5 + self.p6, Polynomial([ + Monomial({}, 7.96783496993343), + Monomial({2: 3, 3: 1}, 1.25), + Monomial({1: -1, 3: 2}) + ])) + + return + + def test_polynomial_subtraction(self): + + self.assertEqual(self.p3 - self.p2, Polynomial([ + Monomial({1: 2, 2: -1}, 1.5) + ])) + + self.assertEqual(self.p3 - self.p3, Polynomial([])) + + self.assertEqual(self.p2 - self.p3, Polynomial([ + Monomial({1: 2, 2: -1}, -1.5) + ])) + + pass + + def test_polynomial_multiplication(self): + self.assertEqual(self.p0 * self.p2, Polynomial([])) + self.assertEqual(self.p1 * self.p2, Polynomial([])) + + self.assertEqual(self.p2 * self.p3, Polynomial([ + Monomial({1: 2}, 4), + Monomial({1: 3, 2: -1}, Fraction(3, 1)) + ])) + return + + def test_polynomial_division(self): + + # Should raise a ValueError if the divisor is not a monomial + # or a polynomial with only one term. + self.assertRaises(ValueError, lambda x, y: x / y, self.p5, self.p3) + self.assertRaises(ValueError, lambda x, y: x / y, self.p6, self.p4) + + self.assertEqual(self.p3 / self.p2, Polynomial([ + Monomial({}, 1), + Monomial({1: 1, 2: -1}, 0.75) + ])) + self.assertEqual(self.p7 / self.m1, Polynomial([ + Monomial({1: -1, 2: -3}, 2), + Monomial({1: 0, 2: -4}, 1.5) + ])) + self.assertEqual(self.p7 / self.m1, Polynomial([ + Monomial({1: -1, 2: -3}, 2), + Monomial({2: -4}, 1.5) + ])) + + quotient, remainder = self.p8.polynomial_division(self.p9) + self.assertEqual(quotient, Polynomial([ + Monomial({1: 2}, 1), + Monomial({1: 1}, 1), + 3 + ])) + self.assertEqual(remainder, Polynomial([5])) + + return + + def test_polynomial_variables(self): + # The zero polynomial has no variables. + + self.assertEqual(self.p0.variables(), set()) + self.assertEqual(self.p1.variables(), set()) + + # The total variables are the union of the variables + # from the monomials. + self.assertEqual(self.p4.variables(), {1, 2, 3}) + + # The monomials with coefficient 0 should be dropped. + self.assertEqual(self.p5.variables(), {1, 3}) + return + + def test_polynomial_subs(self): + # Anything substitued in the zero polynomial + # should evaluate to 0. + self.assertEqual(self.p1.subs(2), 0) + self.assertEqual(self.p0.subs(-101231), 0) + + # Should raise a ValueError if not enough variables are supplied. + self.assertRaises(ValueError, lambda x, y: x.subs(y), self.p4, {1: 3, 2: 2}) + self.assertRaises(ValueError, lambda x, y: x.subs(y), self.p4, {}) + + # Should work fine if a complete subsitution map is provided. + self.assertAlmostEqual(self.p4.subs({1: 1, 2: 1, 3: 1}), (1 + math.pi + Fraction(2, 3)), delta=1e-9) + # Should work fine if more than enough substitutions are provided. + self.assertAlmostEqual(self.p4.subs({1: 1, 2: 1, 3: 1, 4: 1}), (1 + math.pi + Fraction(2, 3)), delta=1e-9) + return + + def test_polynomial_clone(self): + + # The zero polynomial always clones to itself. + self.assertEqual(self.p0.clone(), self.p0) + self.assertEqual(self.p1.clone(), self.p0) + self.assertEqual(self.p0.clone(), self.p1) + self.assertEqual(self.p1.clone(), self.p1) + + # The polynomial should clone nicely. + self.assertEqual(self.p4.clone(), self.p4) + + # The monomial with a zero coefficient should be dropped + # in the clone. + self.assertEqual(self.p5.clone(), Polynomial([ + Monomial({1: -1, 3: 2}, 1) + ])) + return From 0afef529407d3726a83a6700815bb56cb8e24297 Mon Sep 17 00:00:00 2001 From: mantaur Date: Thu, 3 Mar 2022 13:00:50 +0100 Subject: [PATCH 2/3] feat: expand polynomial division expands on functionality to polynomial/polynomial TODO. Test that assert ValueError for multiple variable divisor polynomials have only been commented out and might need an update instead. --- algorithms/maths/polynomial.py | 50 +++++++++++++++++++++++------ tests/test_polynomial.py | 58 +++++++++++++++++----------------- 2 files changed, 69 insertions(+), 39 deletions(-) diff --git a/algorithms/maths/polynomial.py b/algorithms/maths/polynomial.py index 35ff2f227..569ff44d2 100644 --- a/algorithms/maths/polynomial.py +++ b/algorithms/maths/polynomial.py @@ -4,7 +4,7 @@ from typing import Dict, Union, Set, Iterable from numbers import Rational from functools import reduce - +from math import inf class Monomial: """ @@ -289,6 +289,14 @@ def degree(self): """ return sum(self.variables.values()) + def degree_with_respect_to(self, other): + """ + Same as degree(), except if this monomial shares no variables with other, then return -inf. + """ + if not self.all_variables().intersection(other.all_variables()): + return -inf + return self.degree() + class Polynomial: """ @@ -467,30 +475,39 @@ def __truediv__(self, other: Union[int, float, Fraction, Monomial]): def polynomial_division(self, other): """ Perform polynomial division. - """ - variables = self.variables() | other.variables() - if len(variables) > 1: + Order of division is performed following this rating: + 1. Dividend term shares variable with divisor term of higest degree. + 2. Degree + ie: if divisor is xy^2z^3 then a term of the dividend is divisible iff it is a produt of + xy^2z^2 and it is of the same or a (total) higher degree. + """ + # variables = self.variables() | other.variables() + # if len(variables) > 1: # Polynomial division when there's more than one variable turns out # to be hard to define, as the answer depends on the order # of the monomials. # Reference: https://math.stackexchange.com/questions/32070/what-is-the-algorithm-for-long-division-of-polynomials-with-multiple-variables - raise ValueError("cannot divide polynomials containing more than one variable") + # raise ValueError("cannot divide polynomials containing more than one variable") # Implementation using standard long division # Reference: https://en.wikipedia.org/wiki/Polynomial_long_division remainder = self quotient = Polynomial([]) while len(remainder.all_monomials()) > 0: - # Find the monomials with highest degree - remainder_max = max(remainder.all_monomials(), key=lambda m: m.degree()) + # Find the monomial with highest degree in the divisor other_max = max(other.all_monomials(), key=lambda m: m.degree()) - # Continue until the remainder's degree cannot be reduced further - if remainder_max.degree() < other_max.degree(): + # Grab the term/monomial (with the highest degree) from the dividend that shares a + # variable with the highest degree term/monomial from the divisor. + dividend_max = max(remainder.all_monomials(), key = lambda term: term.degree_with_respect_to(other_max)) + + # Continue until the remainder's degree cannot be reduced further, or no common variable between the dividend and divisor remains + # TODO check if both of these conditions are needed?, maybe only second one + if dividend_max.degree() < other_max.degree() or dividend_max.degree_with_respect_to(other_max) == -inf: break # Find the factor required to reduce the remainders degree by one (or more) - factor = remainder_max / other_max + factor = dividend_max / other_max remainder -= other * factor quotient += factor @@ -575,3 +592,16 @@ def __str__(self) -> str: """ return ' + '.join(str(m) for m in self.all_monomials() if m.coeff != Fraction(0, 1)) +def main(): + print("Starting...") + # Test the polynomial class¨ + # 3xy + y + z^2 + dividend = Polynomial([Monomial({'x': 1, 'y': 1}, 3), Monomial({'y': 1}, 1), Monomial({'z': 3}, 1)]) + # 3x + 1 + divisor = Polynomial([Monomial({'x': 1}, 3), 1]) + quotient, remainder = dividend.polynomial_division(divisor) + print(quotient) + print(remainder) + + +main() \ No newline at end of file diff --git a/tests/test_polynomial.py b/tests/test_polynomial.py index 0ea0c61c1..add63a35f 100644 --- a/tests/test_polynomial.py +++ b/tests/test_polynomial.py @@ -115,35 +115,35 @@ def test_polynomial_multiplication(self): ])) return - def test_polynomial_division(self): - - # Should raise a ValueError if the divisor is not a monomial - # or a polynomial with only one term. - self.assertRaises(ValueError, lambda x, y: x / y, self.p5, self.p3) - self.assertRaises(ValueError, lambda x, y: x / y, self.p6, self.p4) - - self.assertEqual(self.p3 / self.p2, Polynomial([ - Monomial({}, 1), - Monomial({1: 1, 2: -1}, 0.75) - ])) - self.assertEqual(self.p7 / self.m1, Polynomial([ - Monomial({1: -1, 2: -3}, 2), - Monomial({1: 0, 2: -4}, 1.5) - ])) - self.assertEqual(self.p7 / self.m1, Polynomial([ - Monomial({1: -1, 2: -3}, 2), - Monomial({2: -4}, 1.5) - ])) - - quotient, remainder = self.p8.polynomial_division(self.p9) - self.assertEqual(quotient, Polynomial([ - Monomial({1: 2}, 1), - Monomial({1: 1}, 1), - 3 - ])) - self.assertEqual(remainder, Polynomial([5])) - - return + # def test_polynomial_division(self): + + # # Should raise a ValueError if the divisor is not a monomial + # # or a polynomial with only one term. + # self.assertRaises(ValueError, lambda x, y: x / y, self.p5, self.p3) + # self.assertRaises(ValueError, lambda x, y: x / y, self.p6, self.p4) + + # self.assertEqual(self.p3 / self.p2, Polynomial([ + # Monomial({}, 1), + # Monomial({1: 1, 2: -1}, 0.75) + # ])) + # self.assertEqual(self.p7 / self.m1, Polynomial([ + # Monomial({1: -1, 2: -3}, 2), + # Monomial({1: 0, 2: -4}, 1.5) + # ])) + # self.assertEqual(self.p7 / self.m1, Polynomial([ + # Monomial({1: -1, 2: -3}, 2), + # Monomial({2: -4}, 1.5) + # ])) + + # quotient, remainder = self.p8.polynomial_division(self.p9) + # self.assertEqual(quotient, Polynomial([ + # Monomial({1: 2}, 1), + # Monomial({1: 1}, 1), + # 3 + # ])) + # self.assertEqual(remainder, Polynomial([5])) + + # return def test_polynomial_variables(self): # The zero polynomial has no variables. From 83392188b062ed4ee00014e194d0fb22d7c4be47 Mon Sep 17 00:00:00 2001 From: Christofer Nolander Date: Thu, 3 Mar 2022 15:19:08 +0100 Subject: [PATCH 3/3] add multivariate polynomial-polynomial division MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Uses a Gröbner-basis in order to generalize euclidean division to polynomials with multiple variables. Co-authored-by: Mark --- algorithms/maths/polynomial.py | 88 +++++++++++++++--------- tests/test_polynomial.py | 121 ++++++++++++++++++++++----------- 2 files changed, 140 insertions(+), 69 deletions(-) diff --git a/algorithms/maths/polynomial.py b/algorithms/maths/polynomial.py index 569ff44d2..cfab590cb 100644 --- a/algorithms/maths/polynomial.py +++ b/algorithms/maths/polynomial.py @@ -293,9 +293,45 @@ def degree_with_respect_to(self, other): """ Same as degree(), except if this monomial shares no variables with other, then return -inf. """ - if not self.all_variables().intersection(other.all_variables()): - return -inf + for other_variable, other_degree in other.variables.items(): + if other_variable not in self.variables: + return -inf + self_degree = self.variables[other_variable] + if self_degree < other_degree: + return -inf + return self.degree() + + def graded_reverse_lexicographic_order(self): + """ + Sorts monomials first by their degree, and then by the reverse order of their variables. + + For example, monomials would be sorted as follows: + + x^2 > xy > y^2 > xz > yz > z^2 + + Reference: https://en.wikipedia.org/wiki/Monomial_order#Graded_reverse_lexicographic_order + """ + return (self.degree(), self.variables_flattened_reverse()) + + def variables_flattened_reverse(self): + """ + Given a monomial, returns the variables names as a list (keeping the + exponent as the number of repetitions). + + For example, the monomial: + x^3 y^2 z^4 + would turn into: + xxxyyzzzz + and then reversed: + zzzzyyxxx + """ + names = [] + for variable, degree in self.variables.items(): + names += [variable] * degree + names.sort() + names.reverse() + return names class Polynomial: @@ -443,20 +479,24 @@ def __mul__(self, other: Union[int, float, Fraction, Monomial]): raise ValueError('Can only multiple int, float, Fraction, Monomials, or Polynomials with Polynomials.') # def __floordiv__(self, other: Union[int, float, Fraction, Monomial, Polynomial]) -> Polynomial: - def __floordiv__(self, other: Union[int, float, Fraction, Monomial]): + def __floordiv__(self, other: Union[int, float, Fraction, Monomial, 'Polynomial']): """ For Polynomials, floordiv is the same as truediv. """ + if isinstance(other, Polynomial): + if len(other.all_monomials()) == 1: + monomial = other.all_monomials().pop() + return self.__truediv__(monomial) + quotient, remainder = self.polynomial_division(other) + return quotient return self.__truediv__(other) # def __truediv__(self, other: Union[int, float, Fraction, Monomial, Polynomial]) -> Polynomial: - def __truediv__(self, other: Union[int, float, Fraction, Monomial]): + def __truediv__(self, other: Union[int, float, Fraction, Monomial, 'Polynomial']): """ For Polynomials, only division by a monomial is defined. - - TODO: Implement polynomial / polynomial. """ if isinstance(other, int) or isinstance(other, float) or isinstance(other, Fraction): return self.__truediv__( Monomial({}, other) ) @@ -468,6 +508,7 @@ def __truediv__(self, other: Union[int, float, Fraction, Monomial]): monomial = other.all_monomials().pop() return self.__truediv__(monomial) quotient, remainder = self.polynomial_division(other) + assert len(remainder.all_monomials()) == 0, "polynomial division yielded non-zero remainder" return quotient raise ValueError('Can only divide a polynomial by an int, float, Fraction, or a Monomial.') @@ -481,29 +522,28 @@ def polynomial_division(self, other): ie: if divisor is xy^2z^3 then a term of the dividend is divisible iff it is a produt of xy^2z^2 and it is of the same or a (total) higher degree. """ - # variables = self.variables() | other.variables() - # if len(variables) > 1: - # Polynomial division when there's more than one variable turns out - # to be hard to define, as the answer depends on the order - # of the monomials. - # Reference: https://math.stackexchange.com/questions/32070/what-is-the-algorithm-for-long-division-of-polynomials-with-multiple-variables - # raise ValueError("cannot divide polynomials containing more than one variable") - # Implementation using standard long division + # Check that all monomial degrees are non-negative + monomials = self.all_monomials() | other.all_monomials() + if any((degree <= 0 for monomial in monomials for degree in monomial.variables.values())): + raise ValueError("cannot divide polynomials with negative or zero exponent") + + # Implementation using standard long division using a Gröbner basis # Reference: https://en.wikipedia.org/wiki/Polynomial_long_division + # Reference: https://en.wikipedia.org/wiki/Gr%C3%B6bner_basis#Reduction + # Reference: https://en.wikipedia.org/wiki/Monomial_order#Graded_reverse_lexicographic_order remainder = self quotient = Polynomial([]) while len(remainder.all_monomials()) > 0: # Find the monomial with highest degree in the divisor - other_max = max(other.all_monomials(), key=lambda m: m.degree()) + other_max = max(other.all_monomials(), key=lambda m: m.graded_reverse_lexicographic_order()) # Grab the term/monomial (with the highest degree) from the dividend that shares a # variable with the highest degree term/monomial from the divisor. dividend_max = max(remainder.all_monomials(), key = lambda term: term.degree_with_respect_to(other_max)) # Continue until the remainder's degree cannot be reduced further, or no common variable between the dividend and divisor remains - # TODO check if both of these conditions are needed?, maybe only second one - if dividend_max.degree() < other_max.degree() or dividend_max.degree_with_respect_to(other_max) == -inf: + if dividend_max.degree_with_respect_to(other_max) == -inf: break # Find the factor required to reduce the remainders degree by one (or more) @@ -591,17 +631,3 @@ def __str__(self) -> str: the polynomial. """ return ' + '.join(str(m) for m in self.all_monomials() if m.coeff != Fraction(0, 1)) - -def main(): - print("Starting...") - # Test the polynomial class¨ - # 3xy + y + z^2 - dividend = Polynomial([Monomial({'x': 1, 'y': 1}, 3), Monomial({'y': 1}, 1), Monomial({'z': 3}, 1)]) - # 3x + 1 - divisor = Polynomial([Monomial({'x': 1}, 3), 1]) - quotient, remainder = dividend.polynomial_division(divisor) - print(quotient) - print(remainder) - - -main() \ No newline at end of file diff --git a/tests/test_polynomial.py b/tests/test_polynomial.py index add63a35f..7e0b3f7b6 100644 --- a/tests/test_polynomial.py +++ b/tests/test_polynomial.py @@ -89,8 +89,6 @@ def test_polynomial_addition(self): Monomial({1: -1, 3: 2}) ])) - return - def test_polynomial_subtraction(self): self.assertEqual(self.p3 - self.p2, Polynomial([ @@ -103,8 +101,6 @@ def test_polynomial_subtraction(self): Monomial({1: 2, 2: -1}, -1.5) ])) - pass - def test_polynomial_multiplication(self): self.assertEqual(self.p0 * self.p2, Polynomial([])) self.assertEqual(self.p1 * self.p2, Polynomial([])) @@ -113,37 +109,89 @@ def test_polynomial_multiplication(self): Monomial({1: 2}, 4), Monomial({1: 3, 2: -1}, Fraction(3, 1)) ])) - return - - # def test_polynomial_division(self): - - # # Should raise a ValueError if the divisor is not a monomial - # # or a polynomial with only one term. - # self.assertRaises(ValueError, lambda x, y: x / y, self.p5, self.p3) - # self.assertRaises(ValueError, lambda x, y: x / y, self.p6, self.p4) - - # self.assertEqual(self.p3 / self.p2, Polynomial([ - # Monomial({}, 1), - # Monomial({1: 1, 2: -1}, 0.75) - # ])) - # self.assertEqual(self.p7 / self.m1, Polynomial([ - # Monomial({1: -1, 2: -3}, 2), - # Monomial({1: 0, 2: -4}, 1.5) - # ])) - # self.assertEqual(self.p7 / self.m1, Polynomial([ - # Monomial({1: -1, 2: -3}, 2), - # Monomial({2: -4}, 1.5) - # ])) - - # quotient, remainder = self.p8.polynomial_division(self.p9) - # self.assertEqual(quotient, Polynomial([ - # Monomial({1: 2}, 1), - # Monomial({1: 1}, 1), - # 3 - # ])) - # self.assertEqual(remainder, Polynomial([5])) - - # return + + def test_polynomial_division(self): + # Should raise a ValueError if any polynomial contains negative exponent + self.assertRaises(ValueError, lambda x, y: x / y, self.p5, self.p3) + + self.assertEqual(self.p3 / self.p2, Polynomial([ + Monomial({}, 1), + Monomial({1: 1, 2: -1}, 0.75) + ])) + self.assertEqual(self.p7 / self.m1, Polynomial([ + Monomial({1: -1, 2: -3}, 2), + Monomial({1: 0, 2: -4}, 1.5) + ])) + self.assertEqual(self.p7 / self.m1, Polynomial([ + Monomial({1: -1, 2: -3}, 2), + Monomial({2: -4}, 1.5) + ])) + + def test_polynomial_division_by_polynomial(self): + """ + Make sure that we can devide two polynomials when there's only one variable + """ + quotient, remainder = self.p8.polynomial_division(self.p9) + self.assertEqual(quotient, Polynomial([ + Monomial({1: 2}, 1), + Monomial({1: 1}, 1), + 3 + ])) + self.assertEqual(remainder, Polynomial([5])) + + def test_polynomial_division_three_variables(self): + """ + Makes sure that we can divide polynomials when there are three different + variables in the denominator + """ + # see: https://math.stackexchange.com/questions/2167213/polynomial-division-in-3-variables + # x^3 - xyz - 2x^2 + 2xy + yz + dividend = Polynomial([ + Monomial({'x': 3}, 1), + Monomial({'x': 1, 'y': 1, 'z': 1}, -1), + Monomial({'x': 2}, -2), + Monomial({'x': 1, 'y': 1}, 2), + Monomial({'y': 1, 'z': 1}, 1), + ]) + # x - 2 + divisor = Polynomial([Monomial({'x': 1}, 1), -2]) + quotient, remainder = dividend.polynomial_division(divisor) + + # Should equal: x^2 - yz + 2y + self.assertEqual(quotient, Polynomial([ + Monomial({'x': 2}, 1), + Monomial({'y': 1, 'z': 1}, -1), + Monomial({'y': 1}, 2), + ])) + + # Should equal: 4y - yz + self.assertEqual(remainder, Polynomial([ + Monomial({'y': 1}, 4), + Monomial({'y': 1, 'z': 1}, -1), + ])) + + def test_polynomial_division_cubic(self): + """ + Makes sure that we can divide polynomials when there are three different + variables in the denominator and numerator + """ + pxyz = Polynomial([ + Monomial({'x':1}, 1), + Monomial({'y':1}, 1), + Monomial({'z':1}, 1) + ]) + pxyz2 = pxyz * pxyz + pxyz3 = pxyz * pxyz * pxyz + + # Test that (x + y + z)^3 / (x + y + z)^2 = (x + y + z) + quotient, remainder = pxyz3.polynomial_division(pxyz2) + self.assertEqual(quotient, pxyz) + self.assertEqual(remainder, Polynomial([])) + + # Test that (x + y + z)^3 / (x + y + z) = (x + y + z)^2 + quotient, remainder = pxyz3.polynomial_division(pxyz) + self.assertEqual(quotient, pxyz2) + self.assertEqual(remainder, Polynomial([])) def test_polynomial_variables(self): # The zero polynomial has no variables. @@ -157,7 +205,6 @@ def test_polynomial_variables(self): # The monomials with coefficient 0 should be dropped. self.assertEqual(self.p5.variables(), {1, 3}) - return def test_polynomial_subs(self): # Anything substitued in the zero polynomial @@ -173,7 +220,6 @@ def test_polynomial_subs(self): self.assertAlmostEqual(self.p4.subs({1: 1, 2: 1, 3: 1}), (1 + math.pi + Fraction(2, 3)), delta=1e-9) # Should work fine if more than enough substitutions are provided. self.assertAlmostEqual(self.p4.subs({1: 1, 2: 1, 3: 1, 4: 1}), (1 + math.pi + Fraction(2, 3)), delta=1e-9) - return def test_polynomial_clone(self): @@ -191,4 +237,3 @@ def test_polynomial_clone(self): self.assertEqual(self.p5.clone(), Polynomial([ Monomial({1: -1, 3: 2}, 1) ])) - return