from __future__ import division

def divide(numerator, denominator):
    """
    Divide two numbers.
    
    Parameters
    ----------
    
    numerator: float
        numerator
    denominator: float
        denominator
        
    Returns
    -------
    
    fraction: float
        numerator / denominator 
    """
    return numerator / denominator

print(divide(4.0, 5.0))

"""
Explanation: Things go wrong when programming all the time. Some of these "problems" are errors that stop the program from making sense. Others are problems that stop the program from working in specific, special cases. These "problems" may be real, or we may want to treat them as special cases that don't stop the program from running.
These special cases can be dealt with using exceptions.
Exceptions
Let's define a function that divides two numbers.
End of explanation
"""


print(divide(4.0, 0.0))

"""
Explanation: But what happens if we try something really stupid?
End of explanation
"""


denominators = [1.0, 0.0, 3.0, 5.0]
for denominator in denominators:
    print(divide(4.0, denominator))

"""
Explanation: So, the code works fine until we pass in input that we shouldn't. When we do, this causes the code to stop. To show how this can be a problem, consider the loop:
End of explanation
"""


try:
    print(divide(4.0, 0.0))
except ZeroDivisionError:
    print("Dividing by zero is a silly thing to do!")

denominators = [1.0, 0.0, 3.0, 5.0]
for denominator in denominators:
    try:
        print(divide(4.0, denominator))
    except ZeroDivisionError:
        print("Dividing by zero is a silly thing to do!")

"""
Explanation: There are three sensible results, but we only get the first.
There are many more complex, real cases where it's not obvious that we're doing something wrong ahead of time. In this case, we want to be able to try running the code and catch errors without stopping the code. This can be done in Python:
End of explanation
"""


try:
    print(divide(4.0, "zero"))
except ZeroDivisionError:
    print("Dividing by zero is a silly thing to do!")

"""
Explanation: The idea here is given by the names. Python will try to execute the code inside the try block. This is just like an if or a for block: each command that is indented in that block will be executed in order.
If, and only if, an error arises then the except block will be checked. If the error that is produced matches the one listed then instead of stopping, the code inside the except block will be run instead.
To show how this works with different errors, consider a different silly error:
End of explanation
"""


try:
    print(divide(4.0, "zero"))
except ZeroDivisionError:
    print("Dividing by zero is a silly thing to do!")
except TypeError:
    print("Dividing by a string is a silly thing to do!")

"""
Explanation: We see that, as it makes no sense to divide by a string, we get a TypeError instead of a ZeroDivisionError. We could catch both errors:
End of explanation
"""


try:
    print(divide(4.0, "zero"))
except:
    print("Some error occured")

"""
Explanation: We could catch any error:
End of explanation
"""


try:
    print(divide(4.0, "zero"))
except (ZeroDivisionError, TypeError) as exception:
    print("Some error occured: {}".format(exception))

"""
Explanation: This doesn't give us much information, and may lose information that we need in order to handle the error. We can capture the exception to a variable, and then use that variable:
End of explanation
"""


denominators = [1.0, 0.0, 3.0, "zero", 5.0]
results = []
divisors = []
for denominator in denominators:
    try:
        result = divide(4.0, denominator)
    except (ZeroDivisionError, TypeError) as exception:
        print("Error of type {} for denominator {}".format(exception, denominator))
    else:
        results.append(result)
        divisors.append(denominator)
print(results)
print(divisors)

"""
Explanation: Here we have caught two possible types of error within the tuple (which must, in this case, have parantheses) and captured the specific error in the variable exception. This variable can then be used: here we just print it out.
Normally best practise is to be as specific as possible on the error you are trying to catch.
Extending the logic
Sometimes you may want to perform an action only if an error did not occur. For example, let's suppose we wanted to store the result of dividing 4 by a divisor, and also store the divisor, but only if the divisor is valid.
One way of doing this would be the following:
End of explanation
"""


def divide_sum(numerator, denominator1, denominator2):
    """
    Divide a number by a sum.
    
    Parameters
    ----------
    
    numerator: float
        numerator
    denominator1: float
        Part of the denominator
    denominator2: float
        Part of the denominator
        
    Returns
    -------
    
    fraction: float
        numerator / (denominator1 + denominator2)
    """
    
    return numerator / (denominator1 + denominator2)

divide_sum(1, 1, -1)

"""
Explanation: The statements in the else block are only run if the try block succeeds. If it doesn't - if the statements in the try block raise an exception - then the statements in the else block are not run.
Exceptions in your own code
Sometimes you don't want to wait for the code to break at a low level, but instead stop when you know things are going to go wrong. This is usually because you can be more informative about what's going wrong. Here's a slightly artificial example:
End of explanation
"""


def divide_sum(numerator, denominator1, denominator2):
    """
    Divide a number by a sum.
    
    Parameters
    ----------
    
    numerator: float
        numerator
    denominator1: float
        Part of the denominator
    denominator2: float
        Part of the denominator
        
    Returns
    -------
    
    fraction: float
        numerator / (denominator1 + denominator2)
    """
    
    if (denominator1 + denominator2) == 0:
        raise ZeroDivisionError("The sum of denominator1 and denominator 2 is zero!")
    
    return numerator / (denominator1 + denominator2)

divide_sum(1, 1, -1)

"""
Explanation: It should be obvious to the code that this is going to go wrong. Rather than letting the code hit the ZeroDivisionError exception automatically, we can raise it ourselves, with a more meaningful error message:
End of explanation
"""


from math import sqrt
    
def real_quadratic_roots(a, b, c):
    """
    Find the real roots of the quadratic equation a x^2 + b x + c = 0, if they exist.
    
    Parameters
    ----------
    
    a : float
        Coefficient of x^2
    b : float
        Coefficient of x^1
    c : float
        Coefficient of x^0
        
    Returns
    -------
    
    roots : tuple
        The roots
        
    Raises
    ------
    
    NotImplementedError
        If the roots are not real.
    """
    
    discriminant = b**2 - 4.0*a*c
    if discriminant < 0.0:
        raise NotImplementedError("The discriminant is {}<0. "
                                  "No real roots exist.".format(discriminant))
    
    x_plus = (-b + sqrt(discriminant)) / (2.0*a)
    x_minus = (-b - sqrt(discriminant)) / (2.0*a)
    
    return x_plus, x_minus

print(real_quadratic_roots(1.0, 5.0, 6.0))

real_quadratic_roots(1.0, 1.0, 5.0)

"""
Explanation: There are a large number of standard exceptions in Python, and most of the time you should use one of those, combined with a meaningful error message. One is particularly useful: NotImplementedError.
This exception is used when the behaviour the code is about to attempt makes no sense, is not defined, or similar. For example, consider computing the roots of the quadratic equation, but restricting to only real solutions. Using the standard formula
\begin{equation}
  x_{\pm} = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}
\end{equation}
we know that this only makes sense if $b^2 \ge 4ac$. We put this in code as:
End of explanation
"""


from math import sqrt
    
def real_quadratic_roots(a, b, c):
    """
    Find the real roots of the quadratic equation a x^2 + b x + c = 0, if they exist.
    
    Parameters
    ----------
    
    a : float
        Coefficient of x^2
    b : float
        Coefficient of x^1
    c : float
        Coefficient of x^0
        
    Returns
    -------
    
    roots : tuple or None
        The roots
    """
    
    discriminant = b**2 - 4.0*a*c
    if discriminant < 0.0:
        return None
    
    x_plus = (-b + sqrt(discriminant)) / (2.0*a)
    x_minus = (-b + sqrt(discriminant)) / (2.0*a)
    
    return x_plus, x_minus

"""
Explanation: Testing
How do we know if our code is working correctly? It is not when the code runs and returns some value: as seen above, there may be times where it makes sense to stop the code even when it is correct, as it is being used incorrectly. We need to test the code to check that it works.
Unit testing is the idea of writing many small tests that check if simple cases are behaving correctly. Rather than trying to prove that the code is correct in all cases (which could be very hard), we check that it is correct in a number of tightly controlled cases (which should be more straightforward). If we later find a problem with the code, we add a test to cover that case.
Consider a function solving for the real roots of the quadratic equation again. This time, if there are no real roots we shall return None (to say there are no roots) instead of raising an exception.
End of explanation
"""


print(real_quadratic_roots(1, 0, 1))

"""
Explanation: First we check what happens if there are imaginary roots, using $x^2 + 1 = 0$:
End of explanation
"""


print(real_quadratic_roots(1, 0, 0))

"""
Explanation: As we wanted, it has returned None. We also check what happens if the roots are zero, using $x^2 = 0$:
End of explanation
"""


print(real_quadratic_roots(1, 0, -1))

"""
Explanation: We get the expected behaviour. We also check what happens if the roots are real, using $x^2 - 1 = 0$ which has roots $\pm 1$:
End of explanation
"""


from math import sqrt
    
def real_quadratic_roots(a, b, c):
    """
    Find the real roots of the quadratic equation a x^2 + b x + c = 0, if they exist.
    
    Parameters
    ----------
    
    a : float
        Coefficient of x^2
    b : float
        Coefficient of x^1
    c : float
        Coefficient of x^0
        
    Returns
    -------
    
    roots : tuple or None
        The roots
    """
    
    discriminant = b**2 - 4.0*a*c
    if discriminant < 0.0:
        return None
    
    x_plus = (-b + sqrt(discriminant)) / (2.0*a)
    x_minus = (-b - sqrt(discriminant)) / (2.0*a)
    
    return x_plus, x_minus

"""
Explanation: Something has gone wrong. Looking at the code, we see that the x_minus line has been copied and pasted from the x_plus line, without changing the sign correctly. So we fix that error:
End of explanation
"""


print(real_quadratic_roots(1, 0, 1))
print(real_quadratic_roots(1, 0, 0))
print(real_quadratic_roots(1, 0, -1))

"""
Explanation: We have changed the code, so now have to re-run all our tests, in case our change broke something else:
End of explanation
"""


print(real_quadratic_roots(0, 1, 1))

"""
Explanation: As a final test, we check what happens if the equation degenerates to a linear equation where $a=0$, using $x + 1 = 0$ with solution $-1$:
End of explanation
"""


from math import sqrt
    
def real_quadratic_roots(a, b, c):
    """
    Find the real roots of the quadratic equation a x^2 + b x + c = 0, if they exist.
    
    Parameters
    ----------
    
    a : float
        Coefficient of x^2
    b : float
        Coefficient of x^1
    c : float
        Coefficient of x^0
        
    Returns
    -------
    
    roots : tuple or float or None
        The root(s) (two if a genuine quadratic, one if linear, None otherwise)
        
    Raises
    ------
    
    NotImplementedError
        If the equation has trivial a and b coefficients, so isn't solvable.
    """
    
    discriminant = b**2 - 4.0*a*c
    if discriminant < 0.0:
        return None
    
    if a == 0:
        if b == 0:
            raise NotImplementedError("Cannot solve quadratic with both a"
                                      " and b coefficients equal to 0.")
        else:
            return -c / b
    
    x_plus = (-b + sqrt(discriminant)) / (2.0*a)
    x_minus = (-b - sqrt(discriminant)) / (2.0*a)
    
    return x_plus, x_minus

"""
Explanation: In this case we get an exception, which we don't want. We fix this problem:
End of explanation
"""


print(real_quadratic_roots(1, 0, 1))
print(real_quadratic_roots(1, 0, 0))
print(real_quadratic_roots(1, 0, -1))
print(real_quadratic_roots(0, 1, 1))

"""
Explanation: And we now must re-run all our tests again, as the code has changed once more:
End of explanation
"""


from numpy.testing import assert_equal, assert_allclose

def test_real_distinct():
    """
    Test that the roots of x^2 - 1 = 0 are \pm 1.
    """
    
    roots = (1.0, -1.0)
    assert_equal(real_quadratic_roots(1, 0, -1), roots,
                 err_msg="Testing x^2-1=0; roots should be 1 and -1.")

test_real_distinct()

"""
Explanation: Formalizing tests
This small set of tests covers most of the cases we are concerned with. However, by this point it's getting hard to remember

what each line is actually testing, and
what the correct value is meant to be.

To formalize this, we write each test as a small function that contains this information for us. Let's start with the $x^2 - 1 = 0$ case where the roots are $\pm 1$:
End of explanation
"""


def test_should_fail():
    """
    Comparing the roots of x^2 - 1 = 0 to (1, 1), which should fail.
    """
    
    roots = (1.0, 1.0)
    assert_equal(real_quadratic_roots(1, 0, -1), roots,
                 err_msg="Testing x^2-1=0; roots should be 1 and 1."
                 " So this test should fail")

test_should_fail()

"""
Explanation: What this function does is checks that the results of the function call match the expected value, here stored in roots. If it didn't match the expected value, it would raise an exception:
End of explanation
"""


from math import sqrt

def test_real_distinct_irrational():
    """
    Test that the roots of x^2 - 2 x + (1 - 10**(-10)) = 0 are 1 \pm 1e-5.
    """
    
    roots = (1 + 1e-5, 1 - 1e-5)
    assert_equal(real_quadratic_roots(1, -2.0, 1.0 - 1e-10), roots,
                 err_msg="Testing x^2-2x+(1-1e-10)=0; roots should be 1 +- 1e-5.")
    
test_real_distinct_irrational()

"""
Explanation: Testing that one floating point number equals another can be dangerous. Consider $x^2 - 2 x + (1 - 10^{-10}) = 0$ with roots $1.1 \pm 10^{-5} )$:
End of explanation
"""


from math import sqrt

def test_real_distinct_irrational():
    """
    Test that the roots of x^2 - 2 x + (1 - 10**(-10)) = 0 are 1 \pm 1e-5.
    """
    
    roots = (1 + 1e-5, 1 - 1e-5)
    assert_allclose(real_quadratic_roots(1, -2.0, 1.0 - 1e-10), roots,
                 err_msg="Testing x^2-2x+(1-1e-10)=0; roots should be 1 +- 1e-5.")
    
test_real_distinct_irrational()

"""
Explanation: We see that the solutions match to the first 14 or so digits, but this isn't enough for them to be exactly the same. In this case, and in most cases using floating point numbers, we want the result to be "close enough": to match the expected precision. There is an assertion for this as well:
End of explanation
"""


from math import sqrt
from numpy.testing import assert_equal, assert_allclose

def test_no_roots():
    """
    Test that the roots of x^2 + 1 = 0 are not real.
    """
    
    roots = None
    assert_equal(real_quadratic_roots(1, 0, 1), roots,
                 err_msg="Testing x^2+1=0; no real roots.")

def test_zero_roots():
    """
    Test that the roots of x^2 = 0 are both zero.
    """
    
    roots = (0, 0)
    assert_equal(real_quadratic_roots(1, 0, 0), roots,
                 err_msg="Testing x^2=0; should both be zero.")

def test_real_distinct():
    """
    Test that the roots of x^2 - 1 = 0 are \pm 1.
    """
    
    roots = (1.0, -1.0)
    assert_equal(real_quadratic_roots(1, 0, -1), roots,
                 err_msg="Testing x^2-1=0; roots should be 1 and -1.")
    
def test_real_distinct_irrational():
    """
    Test that the roots of x^2 - 2 x + (1 - 10**(-10)) = 0 are 1 \pm 1e-5.
    """
    
    roots = (1 + 1e-5, 1 - 1e-5)
    assert_allclose(real_quadratic_roots(1, -2.0, 1.0 - 1e-10), roots,
                 err_msg="Testing x^2-2x+(1-1e-10)=0; roots should be 1 +- 1e-5.")
    
def test_real_linear_degeneracy():
    """
    Test that the root of x + 1 = 0 is -1.
    """
    
    root = -1.0
    assert_equal(real_quadratic_roots(0, 1, 1), root,
                 err_msg="Testing x+1=0; root should be -1.")

test_no_roots()
test_zero_roots()
test_real_distinct()
test_real_distinct_irrational()
test_real_linear_degeneracy()

"""
Explanation: The assert_allclose statement takes options controlling the precision of our test.
We can now write out all our tests:
End of explanation
"""


