From b528efc246c774eb14409d78619a497de7785d75 Mon Sep 17 00:00:00 2001 From: Marek Rychlik Date: Wed, 6 Aug 2025 16:22:34 -0700 Subject: [PATCH] Improve quadratic solver robustness --- Assignment0/basic_no_class_test.py | 6 ++- Assignment0/basic_test.py | 14 ++--- Assignment0/solve_quadratic_equation.py | 71 +++++++++++++++++-------- 3 files changed, 59 insertions(+), 32 deletions(-) diff --git a/Assignment0/basic_no_class_test.py b/Assignment0/basic_no_class_test.py index c411002..e5dadfb 100644 --- a/Assignment0/basic_no_class_test.py +++ b/Assignment0/basic_no_class_test.py @@ -12,6 +12,8 @@ def test_something(): roots = quadratic.solve_quadratic_equation(1, -1000000.001, 1) - # print("Testing without classes/unittest, indeed...") - assert( abs(1000000 - roots[0] ) < 1e-7 ) + # The smaller root is returned first. Its magnitude is around 1e-6 and + # the product of both roots should equal ``c/a`` which is 1. + assert abs(1e-6 - roots[0]) < 1e-12 + assert abs(roots[0] * roots[1] - 1.0) < 1e-6 diff --git a/Assignment0/basic_test.py b/Assignment0/basic_test.py index 2cf918f..24ec989 100644 --- a/Assignment0/basic_test.py +++ b/Assignment0/basic_test.py @@ -21,9 +21,10 @@ def test_easy_case(self): def test_big_coefficient(self): roots = quadratic.solve_quadratic_equation(1, -1000000.001, 1) - # print("Testing with unittest, indeed...") - self.assertAlmostEqual(roots[0], 1000000, places=7, msg = "Not enough places") - self.assertAlmostEqual(roots[0], 1000000, delta=1e-7, msg = "Delta not met") + # The function should return the smaller root first and avoid the + # catastrophic cancellation that occurs for large coefficients. + self.assertAlmostEqual(1e-6, roots[0], places=12) + self.assertAlmostEqual(1.0, roots[0] * roots[1], places=6) def test_double_root_case(self): """Solving a quadratic equation with a repeated root.""" @@ -35,7 +36,6 @@ def test_double_root_case(self): def test_degenerate_quadratic_case(self): """Solving a quadratic with a=0.""" a, b, c = 0, 1, 1 - try: - x1, x2 = quadratic.solve_quadratic_equation(a, b, c) - except ZeroDivisionError: - self.fail("Unhandled division by 0 when a=0.") + x1, x2 = quadratic.solve_quadratic_equation(a, b, c) + self.assertAlmostEqual(-1, x1) + self.assertIsNone(x2) diff --git a/Assignment0/solve_quadratic_equation.py b/Assignment0/solve_quadratic_equation.py index 13479ff..bab76bf 100644 --- a/Assignment0/solve_quadratic_equation.py +++ b/Assignment0/solve_quadratic_equation.py @@ -10,35 +10,60 @@ # A basic quadratic equation solver. High-school method. import math + + def solve_quadratic_equation(a, b, c): + """Solve the quadratic equation ``a*x^2 + b*x + c = 0``. + + The implementation prefers numerical stability and always returns real + roots. A repeated root is represented by ``(root, None)``. Linear + equations (``a = 0``) are supported as a special case. + + Parameters + ---------- + a : float + Coefficient of :math:`x^2`. + b : float + Coefficient of :math:`x`. + c : float + Constant term. + + Returns + ------- + tuple + ``(root1, root2)`` where ``root1`` is the smaller root. ``root2`` is + ``None`` when the equation has a repeated root or is linear. + + Raises + ------ + ValueError + If the equation has complex roots or is degenerate (``a = b = 0``). """ - Solve the quadratic equation a*x^2 + b*x + c = 0 using the standard quadratic formula. - - This function calculates the roots using the basic quadratic formula without any adjustments - for numerical stability. It assumes real coefficients and only returns real roots. - - Parameters: - a (float): Coefficient of x^2. - b (float): Coefficient of x. - c (float): Constant term. - - Returns: - tuple: - - (float): The first root. - - (float or None): The second root, or None if there is only one distinct root due to a zero discriminant. - - Raises: - """ - # Calculate the discriminant - discriminant = b**2 - 4*a*c - # Calculate the discriminant's square root + # Handle linear equations first to avoid division by zero. + if a == 0: + if b == 0: + raise ValueError("Not an equation: both 'a' and 'b' are zero") + return (-c / b, None) + + # Calculate the discriminant and check for real roots + discriminant = b * b - 4 * a * c + if discriminant < 0: + raise ValueError("The equation has complex roots") + sqrt_discriminant = math.sqrt(discriminant) - # Compute both roots using the standard quadratic formula - root1 = (-b + sqrt_discriminant) / (2 * a) - root2 = (-b - sqrt_discriminant) / (2 * a) + # Repeated root + if discriminant == 0: + return (-b / (2 * a), None) + + # Use a numerically stable version of the quadratic formula + q = -0.5 * (b + math.copysign(sqrt_discriminant, b)) + root1 = q / a + root2 = c / q + # Ensure the roots are ordered from smallest to largest + root1, root2 = (root1, root2) if root1 <= root2 else (root2, root1) return (root1, root2) # Example usage: # NOTE: Also, as simple testing framework.