diff --git a/polynomial/__init__.py b/polynomial/__init__.py index f46d049..09211d0 100644 --- a/polynomial/__init__.py +++ b/polynomial/__init__.py @@ -14,3 +14,4 @@ from .frozen import FrozenPolynomial, ZeroPolynomial from .binomial import Binomial, LinearBinomial from .trinomial import Trinomial, QuadraticTrinomial +from .quadrinomial import Quadrinomial, CubicQuadrinomial diff --git a/polynomial/quadrinomial.py b/polynomial/quadrinomial.py new file mode 100644 index 0000000..90b9653 --- /dev/null +++ b/polynomial/quadrinomial.py @@ -0,0 +1,94 @@ +"""This module defines different types of quadrinomials and their methods.""" + +from cmath import sqrt as csqrt +from polynomial.core import ( + Polynomial, + Monomial, + Constant, + FixedDegreePolynomial, + FixedTermPolynomial +) + + +class Quadrinomial(FixedTermPolynomial, valid_term_counts=(0, 1, 2, 3, 4)): + """Implements single-variable mathematical trinomials.""" + + def __init__(self, + monomial1=None, + monomial2=None, + monomial3=None, + monomial4=None): + """Initialize the quadrinomial with 4 monomials. + + The arguments can also be 2-tuples in the form: + (coefficient, degree) + """ + if not monomial1: + monomial1 = Monomial(1, 1) + if not monomial2: + monomial2 = Monomial(1, 2) + if not monomial3: + monomial3 = Monomial(1, 3) + if not monomial4: + monomial4 = Monomial(1, 4) + args = [monomial1, monomial2, monomial3, monomial4] + Polynomial.__init__(self, args, from_monomials=True) + + def __repr__(self): + """Return repr(self).""" + terms = self.terms + assert len(terms) == 4 + t1, t2, t3, t4 = terms + return ( + "Quadrinomial(Monomial({0}, {1}), Monomial({2}, {3}), " + "Monomial({4}, {5}), Monomial({6}, {7}))" + .format(*t1, *t2, *t3, *t4) + ) + + +class CubicQuadrinomial(FixedDegreePolynomial, Quadrinomial, valid_degrees=3): + """Implements cubic polynomials and their related methods.""" + + def __init__(self, a=1, b=1, c=1, d=1): + """Initialize the quadrinomial as ax^3 + bx^2 + cx + d.""" + if a == 0: + raise ValueError("Object not a cubic quadrinomial since a==0!") + Polynomial.__init__(self, a, b, c, d) + + @property + def complex_roots(self): + """Return a 3-tuple with the complex roots of ax^3 + bx^2 + cx + d = 0. + + This method uses the general cubic roots formula. + """ + a, b, c, d = self.a, self.b, self.c, self.d + + delta0 = b*b - 3*a*c + delta1 = 2*b*b*b - 9*a*b*c + 27*a*a*d + cardano1 = ((delta1 + csqrt(delta1**2 - 4*(delta0**3))) / 2) ** (1/3) + cardano2 = ((delta1 - csqrt(delta1**2 - 4*(delta0**3))) / 2) ** (1/3) + + cardano = cardano2 if not cardano1 else cardano1 + + xi = (-1 + csqrt(-3)) / 2 + + roots = [(b + (xi**k)*cardano + (delta0 / (xi**k)*cardano)) / (-3 * a) + for k in range(3)] + + return tuple(roots) + + @property + def complex_factors(self): + """Return (a, (x-x_0), (x-x_1), (x-x_2)), where x_i are the roots.""" + roots = self.complex_roots + return (Constant(self.a), + Polynomial(1, -roots[0]), + Polynomial(1, -roots[1]), + Polynomial(1, -roots[2])) + + def __repr__(self): + """Return repr(self).""" + return ( + "CubicQuadrinomial({0!r}, {1!r}, {2!r}, {3!r})" + .format(self.a, self.b, self.c, self.d) + ) diff --git a/tests/test_polynomials_init.py b/tests/test_polynomials_init.py index 886d3b8..5cd43c3 100644 --- a/tests/test_polynomials_init.py +++ b/tests/test_polynomials_init.py @@ -9,6 +9,8 @@ LinearBinomial, Trinomial, QuadraticTrinomial, + Quadrinomial, + CubicQuadrinomial, ) @@ -152,6 +154,42 @@ def test_quadratic_trinomial_default_init(self): self.assertEqual(expected, qt) + def test_quadrinomial_init_from_monomials(self): + """Ensure a quadrinomial is successfully initialized from monomials.""" + m1 = Monomial(3, 3) + m2 = Monomial(4, 4) + m3 = Monomial(5, 5) + expected = Polynomial([m1, m2, m3], from_monomials=True) + + t = Trinomial(m1, m2, m3) + + self.assertEqual(expected, t) + + def test_quadrinomial_default_init(self): + """Test that the default quadrinomial is 'x^4 + x^3 + x^2 + x'.""" + expected = Polynomial(1, 1, 1, 1, 0) + + q = Quadrinomial() + + self.assertEqual(expected, q) + + def test_cubic_quadrinomial_init(self): + """Test that a cubic quadrinomial is successfully initialized.""" + a, b, c, d = 2, 3, 4, 5 + expected = Polynomial(a, b, c, d) + + cq = CubicQuadrinomial(a, b, c, d) + + self.assertEqual(expected, cq) + + def test_cubic_quadrinomial_default_init(self): + """Test that the default cubic quadrinomial is 'x^3 + x^2 + x + 1'.""" + expected = Polynomial(1, 1, 1, 1) + + cq = CubicQuadrinomial() + + self.assertEqual(expected, cq) + def test_linear_binomial_fails_leading_zero(self): """Test that LinearBinomial(0, ?) raises a ValueError.""" self.assertRaises(ValueError, LinearBinomial, 0, 1) @@ -160,6 +198,10 @@ def test_quadratic_trinomial_fails_leading_zero(self): """Test that QuadraticTrinomial(0, ?) raises a ValueError.""" self.assertRaises(ValueError, QuadraticTrinomial, 0, 1) + def test_cubic_quadrinomial_fails_leading_zero(self): + """Test that CubicQuadrinomial(0, ?) raises a ValueError.""" + self.assertRaises(ValueError, CubicQuadrinomial, 0, 1) + def test_monomial_degree_positive_int(self): """Test that monomial only accepts a positive int.""" self.assertRaises(ValueError, Monomial, 1, -1) diff --git a/tests/test_polynomials_operations.py b/tests/test_polynomials_operations.py index bcb04e4..c67a55e 100644 --- a/tests/test_polynomials_operations.py +++ b/tests/test_polynomials_operations.py @@ -12,6 +12,7 @@ ZeroPolynomial, LinearBinomial, QuadraticTrinomial, + CubicQuadrinomial, DegreeError, TermError, ) @@ -730,6 +731,36 @@ def test_negative_discriminant_roots(self): self.assertEqual(exp_real_factors, res_real_factors) self.assertEqual(exp_complex_factors, res_complex_factors) + def test_cubic_formula_triple_root_correct(self): + """Test that the cubic formula correctly computes a triple root.""" + cq = CubicQuadrinomial(1, -3j, -3, 1j) + exp_complex_roots = (1j, 1j, 1j) + exp_complex_factors = (1, + LinearBinomial(1, -1j), + LinearBinomial(1, -1j), + LinearBinomial(1, -1j)) + + res_complex_roots = cq.complex_roots + res_complex_factors = cq.complex_factors + + self.assertEqual(exp_complex_roots, res_complex_roots) + self.assertEqual(exp_complex_factors, res_complex_factors) + + def test_cubic_formula_double_root_correct(self): + """Test that the cubic formula correctly computes a double root.""" + cq = CubicQuadrinomial(1, -4, 5, -2) + exp_complex_roots = (1, 1, 2) + exp_complex_factors = (1, + LinearBinomial(1, -1), + LinearBinomial(1, -1), + LinearBinomial(1, -2)) + + res_complex_roots = cq.complex_roots + res_complex_factors = cq.complex_factors + + self.assertEqual(exp_complex_roots, res_complex_roots) + self.assertEqual(exp_complex_factors, res_complex_factors) + def test_zero_instance_mutable(self): """Test that zero instances are mutable.""" zp = Polynomial.zero_instance() @@ -972,6 +1003,7 @@ def test_setting_empty_terms_mutable_degree(self): def test_setting_empty_terms_immutable_degree(self): """Test setting empty terms.""" immutable = [ + CubicQuadrinomial(1, 2, 3, 4), QuadraticTrinomial(1, 2, 3), LinearBinomial(1, 2), ] @@ -1021,6 +1053,7 @@ def test_pos(self): d = ZeroPolynomial() e = QuadraticTrinomial(1, 3, 7) f = LinearBinomial(9, 2) + g = CubicQuadrinomial(5, 4, 1, 7) self._assert_polynomials_are_the_same(a, +a) self._assert_polynomials_are_the_same(b, +b) @@ -1028,6 +1061,7 @@ def test_pos(self): self._assert_polynomials_are_the_same(d, +d) self._assert_polynomials_are_the_same(e, +e) self._assert_polynomials_are_the_same(f, +f) + self._assert_polynomials_are_the_same(g, +g) def test_div_by_zero(self): """Test that division by 0 is not possible.""" @@ -1038,6 +1072,7 @@ def test_div_by_zero(self): ZeroPolynomial(), QuadraticTrinomial(1, 3, 7), LinearBinomial(9, 2), + CubicQuadrinomial(5, 4, 1, 7), ] for val in to_test: @@ -1056,6 +1091,7 @@ def test_pow_by_negative(self): ZeroPolynomial(), QuadraticTrinomial(1, 3, 7), LinearBinomial(9, 2), + CubicQuadrinomial(5, 4, 1, 7), ] for val in to_test: @@ -1071,6 +1107,7 @@ def test_pow_by_non_integer(self): ZeroPolynomial(), QuadraticTrinomial(1, 3, 7), LinearBinomial(9, 2), + CubicQuadrinomial(5, 4, 1, 7), ] for val in to_test: @@ -1109,6 +1146,7 @@ def test_inequality(self): ZeroPolynomial(), QuadraticTrinomial(1, 3, 7), LinearBinomial(9, 2), + CubicQuadrinomial(5, 4, 1, 7), 7 ] @@ -1144,6 +1182,7 @@ def test_ipow_zero_gives_one(self): ZeroPolynomial(), QuadraticTrinomial(1, 3, 7), LinearBinomial(9, 2), + CubicQuadrinomial(5, 4, 1, 7), ] one_maps = { @@ -1153,6 +1192,7 @@ def test_ipow_zero_gives_one(self): ZeroPolynomial: Constant(1), QuadraticTrinomial: Polynomial(1), LinearBinomial: Polynomial(1), + CubicQuadrinomial: Polynomial(1), } for val in to_test: @@ -1334,12 +1374,16 @@ def test_setitem_raises_error(self): """Test that setitem raises error on invalid inputs.""" lb = LinearBinomial(5, 1) qt = QuadraticTrinomial(1, 2, 3) + cq = CubicQuadrinomial(4, 3, 2, 1) self.assertRaises(DegreeError, lb.__setattr__, "a", 0) self.assertRaises(DegreeError, qt.__setattr__, "a", 0) + self.assertRaises(DegreeError, cq.__setattr__, "a", 0) lb = LinearBinomial(5, 1) qt = QuadraticTrinomial(1, 2, 3) + cq = CubicQuadrinomial(4, 3, 2, 1) self.assertRaises(DegreeError, lb.__setitem__, 1, 0) self.assertRaises(DegreeError, qt.__setitem__, 2, 0) + self.assertRaises(DegreeError, cq.__setitem__, 3, 0) def test_monomial_can_only_have_one_or_no_terms(self): """Test that setting terms works correctly.""" diff --git a/tests/test_polynomials_repr.py b/tests/test_polynomials_repr.py index 399a81d..36bd0ca 100644 --- a/tests/test_polynomials_repr.py +++ b/tests/test_polynomials_repr.py @@ -10,10 +10,12 @@ Monomial, Polynomial, QuadraticTrinomial, + CubicQuadrinomial, ZeroPolynomial, FrozenPolynomial, Trinomial, - Binomial + Binomial, + Quadrinomial ) @@ -92,6 +94,23 @@ def test_quadratic_trinomial(self): self.assertEqual(expect, r) + def test_quadrinomial(self): + """Test that repr() output of a Quadrinomial is valid.""" + expect = "Quadrinomial(Monomial(6, 9), Monomial(5, 5), "\ + "Monomial(2, 3), Monomial(1, 1))" + + r = repr(Quadrinomial((6, 9), (5, 5), (2, 3), (1, 1))) + + self.assertEqual(expect, r) + + def test_cubic_quadrinomial(self): + """Test that repr() output of a CubicQuadrinomial is valid.""" + expect = "CubicQuadrinomial(1, -4, 2, 0)" + + r = repr(CubicQuadrinomial(1, -4, 2, 0)) + + self.assertEqual(expect, r) + def test_frozen(self): """Test that repr() output of a FrozenPolynomial is valid.""" expect = "FrozenPolynomial(1, 2, 3)"