# Solutions

## Question 1

> `1`. Use the class created in {ref}`objects_tutorial` to find the roots of the
>  following quadratics:

> `1`. $f(x) = -4x ^ 2 + x + 6$

First we define the class:


```python
import math


class QuadraticExpression:
    """A class for a quadratic expression"""

    def __init__(self, a, b, c):
        self.a = a
        self.b = b
        self.c = c
        self.discriminant = self.b ** 2 - 4 * self.a * self.c

    def get_roots(self):
        """Return the real valued roots of the quadratic expression"""
        if self.discriminant >= 0:
            x1 = -(self.b + math.sqrt(self.discriminant)) / (2 * self.a)
            x2 = -(self.b - math.sqrt(self.discriminant)) / (2 * self.a)
            return x1, x2
        return ()

    def __add__(self, other):
        """A magic method: let's us have addition between expressions"""
        return QuadraticExpression(self.a + other.a, self.b + other.b, self.c + other.c)

    def __repr__(self):
        """A magic method: changes the default way an instance is displayed"""
        return f"Quadratic expression: {self.a} x ^ 2 + {self.b} x + {self.c}"


class QuadraticExpressionWithAllRoots(QuadraticExpression):
    """
    A class for a quadratic expression that can return imaginary roots

    The `get_roots` function returns two tuples of the form (re, im) where re is
    the real part and im is the imaginary part.
    """

    def get_roots(self):
        """Return the real valued roots of the quadratic expression"""
        if self.discriminant >= 0:
            x1 = -(self.b + math.sqrt(self.discriminant)) / (2 * self.a)
            x2 = -(self.b - math.sqrt(self.discriminant)) / (2 * self.a)
            return (x1, 0), (x2, 0)

        real_part = self.b / (2 * self.a)
        im1 = math.sqrt(-self.discriminant) / (2 * self.a)
        im2 = -math.sqrt(-self.discriminant) / (2 * self.a)
        return ((real_part, im1), (real_part, im2))

    def __add__(self, other):
        """A special method: let's us have addition between expressions"""
        return QuadraticExpressionWithAllRoots(
            self.a + other.a, self.b + other.b, self.c + other.c
        )
```

Now we use it:


```python
f = QuadraticExpressionWithAllRoots(a=-4, b=1, c=6)
f.get_roots()
```




    ((1.356107225224513, 0), (-1.106107225224513, 0))



> `2`. $g(x) = 3x^2 - 6$


```python
g = QuadraticExpressionWithAllRoots(a=3, b=0, c=-6)
g.get_roots()
```




    ((-1.414213562373095, 0), (1.414213562373095, 0))



> `3`. $h(x) = f(x) + g(x)$


```python
h = f + g
h.get_roots()
```




    ((1.0, 0), (0.0, 0))



## Question 2

> `2`. Write a class for a Linear expression and use it to find the roots of the
>  following expressions:

>  `1`. $f(x) = 2x + 6$

First we define the class:


```python
import math


class LinearExpression:
    """A class for a linear expression a x + b"""

    def __init__(self, a, b):
        self.a = a
        self.b = b

    def get_roots(self):
        """Return the roots of the linear expression"""
        if self.a != 0:
            return -self.b / self.a
        return None

    def __add__(self, other):
        """A magic method: let's us have addition between expressions"""
        return LinearExpression(self.a + other.a, self.b + other.b)

    def __repr__(self):
        """A magic method: changes the default way an instance is displayed"""
        return f"Linear expression: {self.a} x + {self.b}"
```

Now we use it:


```python
f = LinearExpression(a=2, b=6)
f.get_roots()
```




    -3.0



> `2`. $g(x) = 3x - 6$


```python
g = LinearExpression(a=3, b=-6)
g.get_roots()
```




    2.0



> `3`. $h(x) = f(x) + g(x)$


```python
h = f + g
h.get_roots()
```




    0.0



## Question 3

> `3`. If rain drops were to fall randomly on a square of side length $2r$ the
>  probability of the drops landing in an inscribed circle of radius $r$ would
>  be given by:

>  $$
      P = \frac{\text{Area of circle}}{\text{Area of square}}=\frac{\pi r ^2}{4r^2}=\frac{\pi}{4}
  $$

>  Thus, if we can approximate $P$ then we can approximate $\pi$ as $4P$. In this
>  question we will write code to approximate $P$ using the random library.

>  First create the following class:

>  ```
>  class Drop:
>      """
>      A class used to represent a random rain drop falling on a square of
>      length r.
>      """
>
>      def __init__(self, r=1):
>          self.x = (0.5 - random.random()) * 2 * r
>          self.y = (0.5 - random.random()) * 2 * r
>          self.in_circle = (self.y) ** 2 + (self.x) ** 2 <= r ** 2
>  ```


```python
import random


class Drop:
    """
    A class used to represent a random rain drop falling on a square of
    length r.
    """

    def __init__(self, r=1):
        self.x = (0.5 - random.random()) * 2 * r
        self.y = (0.5 - random.random()) * 2 * r
        self.in_circle = (self.y) ** 2 + (self.x) ** 2 <= r ** 2
```

>  To approximate $P$ create $N=1000$ instances of Drops and count the
>  number of those that are in the circle. Use this to approximate $\pi$.

We start by creating the required number of drops:


```python
number_of_instances = 10000
random.seed(0)
drops = [Drop() for number in range(number_of_instances)]
```

Now we count the number in the circle:


```python
number_in_circle = len([drop for drop in drops if drop.in_circle])
number_in_circle
```




    7809



The number in the circle leads to the probability $P$:


```python
P = number_in_circle / number_of_instances
```

And $\pi$ can be approximated:


```python
4 * P
```




    3.1236



## Question 4

> `4`. In a similar fashion to question 3, approximate the integral
>  $\int_{0}^11-x^2\;dx$. Recall that the integral corresponds to the area
>  under a curve.

We create a different drop class changing the `in_circle` attribute to
`under_curve` and simplifying where the `x` and `y` are sampled from.


```python
class Drop:
    """
    A class used to represent a random rain drop falling on a square of
    length 1.
    """

    def __init__(self):
        self.x = random.random()
        self.y = random.random()
        self.under_curve = self.y <= 1 - self.x ** 2
```

Now we repeat the steps of question 3:


```python
number_of_instances = 10000
random.seed(0)
drops = [Drop() for number in range(number_of_instances)]
```

Now we count the number in the circle:


```python
number_under_curve = len([drop for drop in drops if drop.under_curve])
number_under_curve
```




    6646



In this particular problem the area of the square is 1 so the probability of
being under the curve is equal to the 1: $P=\frac{\int_{0}^11-x^2\;dx}{1}$.


```python
number_under_curve / number_of_instances
```




    0.6646



We can confirm this:


```python
import sympy as sym

x = sym.Symbol("x")
sym.integrate(1 - x ** 2, (x, 0, 1))
```




$\displaystyle \frac{2}{3}$



5111d8ed79128d1bd99e3b4f10de4e5983ac66b0
ipynb
Jupyter Notebook
book/building-tools/03-objects/solutions/.main.md.bcp.ipynb
11michalis11/pfm
c91b1eda70d7cde3fbe065db4667f84853947850
MIT
2020-09-24T21:02:41.000Z
2020-10-14T08:37:21.000Z
book/building-tools/03-objects/solutions/.main.md.bcp.ipynb
11michalis11/pfm
c91b1eda70d7cde3fbe065db4667f84853947850
MIT
2020-09-21T15:54:23.000Z
2021-12-19T23:26:15.000Z
book/building-tools/03-objects/solutions/.main.md.bcp.ipynb
11michalis11/pfm
c91b1eda70d7cde3fbe065db4667f84853947850
MIT
2020-10-02T09:21:27.000Z
2021-07-08T14:46:27.000Z
Qwen/Qwen-72B
1. YES
2. YES