Improved Quadratic Equation Solver City University of Hong Kong
import math
import numpy as np
from ipywidgets import interact
if not input('Load AIMagics? [Y/n]').lower()=='n':
%reload_ext jupyter_ai_magic_commands
In this notebook, we will improve the quadratic equation solver in the previous lab using conditional executions.
Recall that a quadratic equation is
a x 2 + b x + c = 0 ax^2+bx+c=0 a x 2 + b x + c = 0 where a a a , b b b , and c c c are real-valued coefficients, and x x x is the unknown variable.
The discriminant Δ \Delta Δ discriminates the roots
− b ± Δ 2 a \frac{-b\pm \sqrt{\Delta}}{2a} 2 a − b ± Δ of a quadratic equation:
The roots of a quadratic equation (1) are both equal to
if and only if the discriminant is zero, i.e., Δ = 0 \Delta=0 Δ = 0 .
For example, if ( a , b , c ) = ( 1 , − 2 , 1 ) (a,b,c)=(1,-2,1) ( a , b , c ) = ( 1 , − 2 , 1 ) , then
Δ = ( − 2 ) 2 − 4 ( 1 ⋅ 1 ) = 0 \Delta = (-2)^2 - 4(1\cdot 1) = 0 Δ = ( − 2 ) 2 − 4 ( 1 ⋅ 1 ) = 0 and so the repeated root is
− b 2 a = 2 2 = 1. - \frac{b}{2a} = \frac{2}2 = 1. − 2 a b = 2 2 = 1. ( x − 1 ) 2 = x 2 − 2 x + 1 (x-1)^2 = x^2 - 2x + 1 ( x − 1 ) 2 = x 2 − 2 x + 1 as expected.
We will improve the quadratic solver to return only one root when the discriminant is close enough to 0.
To determine whether a floating point number x x x is close to 0, the implementation math.isclose is insufficient:
The test math.isclose(x, 0, rel_tol=rel_tol) is ineffective as explained in the lecture as it boils down to x==0 or rel_tol>=1.
The test math.isclose(x, 0, abs_tol=1e-9) may not work as expected since x (and therefore its precision error) floats at different scale according to its exponent.
Complete the following function to implement an alternative test of how close x x x is to 0 relative to y y y and z z z with a relative tolerance of δ rel \delta_{\text{rel}} δ rel :
∣ x ∣ ≤ max { ∣ y ∣ , ∣ z ∣ } δ rel \lvert x\rvert\leq \max\Set{\lvert y\rvert, \lvert z\rvert} \delta_{\text{rel}} ∣ x ∣ ≤ max { ∣ y ∣ , ∣ z ∣ } δ rel This test is similar to math.isclose, where δ rel ≥ 0 \delta_{\text{rel}}\geq 0 δ rel ≥ 0 is the relative tolerance specified by the keyword-only argument rel_tol, which has a default value of 1e-9.
def iszero(x, y, z, *, rel_tol=1e-9):
# YOUR CODE HERE
raise NotImplementedError
# tests
assert not iszero(1e-8, 0, 1)
assert iszero(1e-8, 1, 0, rel_tol=1e-7)
assert iszero(1e-8, 1, 10)
Complete the following function by assigning to the variableroots a single root when the discriminant is close to zero relative to b**2 and 4ac, using the relative tolerence rel_tol that defaults to 1e-9. For example, if (a, b, c) == (1, -2, 1), then roots should be assigned the value 1.0 instead of 1.0, 1.0.
def get_roots(a, b, c, *, rel_tol=1e-9):
if iszero(d:=((y:=b**2)-(z:=4*a*c)), y, z, rel_tol=rel_tol):
# YOUR CODE HERE
raise NotImplementedError
return roots
# tests
assert np.isclose(get_roots(1, 1, 0), (-1.0, 0.0)).all()
assert np.isclose(get_roots(1, 0, 0), 0.0).all()
assert np.isclose(get_roots(1, 1e-9, 0), 0.0).all()
assert np.isclose(get_roots(1, 1e-6, 0), (-1e-06, 0.0)).all()
To check whether the root is repeated, why is it better to use iszero(d:=((y:=b**2)-(z:=4*a*c)), y, z, rel_tol=rel_tol) instead of d:=((y:=b**2)-(z:=4*a*c) == 0?
Nevertheless, the quadratic equation remains valid:
the root of which is x = − c b x = -\frac{c}b x = − b c .
def get_roots(a, b, c, *, rel_tol=1e-9):
# YOUR CODE HERE
raise NotImplementedError
return roots
# tests
assert np.isclose(get_roots(1, 1, 0), (-1.0, 0.0)).all()
assert np.isclose(get_roots(1, 0, 0), 0.0).all()
assert np.isclose(get_roots(1, -2, 1), 1.0).all()
assert np.isclose(get_roots(0, -2, 1), 0.5).all()
What if a = b = 0 a=b=0 a = b = 0 ? In this case, the equation becomes
which is always satisfied if c = 0 c=0 c = 0 but never satisfied if c ≠ 0 c\neq 0 c = 0 .
Improve the function get_roots to return root(s) under all the following cases:
If a = 0 a=0 a = 0 and b ≠ 0 b\neq 0 b = 0 , assign roots to the single root − c b -\frac{c}{b} − b c .
If a = b = 0 a=b=0 a = b = 0 and c ≠ 0 c\neq 0 c = 0 , assign roots to None.
If a = b = c = 0 a=b=c=0 a = b = c = 0 , there are infinitely many roots. Assign to roots the tuple -float('inf'), float('inf'). Note that float('inf') converts the string 'inf' to a floating point value that represents ∞ \infty ∞ .
The above checks of whether a coefficient is 0 should be relative to other coefficients with a relative tolerance of rel_tol.
None is a Python keyword that refers to a special object. You should NOT regard it as a string or quote it like "None".
float('inf') converts the string 'inf' to a floating point number no smaller than any other floating point numbers. You should NOT write inf, which needs not refer to float('inf').
def get_roots(a, b, c, *, rel_tol=1e-9):
# YOUR CODE HERE
raise NotImplementedError
return roots
# tests
assert np.isclose(get_roots(1, 1, 0), (-1.0, 0.0)).all()
assert np.isclose(get_roots(1, 0, 0), 0.0).all()
assert np.isclose(get_roots(1, -2, 1), 1.0).all()
assert np.isclose(get_roots(0, -2, 1), 0.5).all()
assert np.isclose(get_roots(0, 0, 0), (-float('inf'), float('inf'))).all()
assert get_roots(0, 0, 1) is None
After you have complete the exercises, you can run your robust solver below:
# quadratic equations solver
@interact(a=(-10, 10, 1), b=(-10, 10, 1), c=(-10, 10, 1))
def quadratic_equation_solver(a=1, b=2, c=1):
print("Root(s):", get_roots(a, b, c))