# Quadratic Equations

Consider the following equation:

\begin{equation}y = 2(x - 1)(x + 2)\end{equation}

If you multiply out the factored ***x*** expressions, this equates to:

\begin{equation}y = 2x^{2} + 2x - 4\end{equation}

Note that the highest ordered term includes a squared variable (x2).

Let's graph this equation for a range of ***x*** values:

```python
import pandas as pd

# Create a dataframe with an x column containing values to plot
df = pd.DataFrame ({'x': range(-9, 9)})

# Add a y column by applying the quadratic equation to x
df['y'] = 2*df['x']**2 + 2 *df['x'] - 4

# Plot the line
%matplotlib inline
from matplotlib import pyplot as plt

plt.plot(df.x, df.y, color="grey")
plt.xlabel('x')
plt.ylabel('y')
plt.grid()
plt.axhline()
plt.axvline()
plt.show()
```

Note that the graph shows a *parabola*, which is an arc-shaped line that reflects the x and y values calculated for the equation.

Now let's look at another equation that includes an ***x2*** term:

\begin{equation}y = -2x^{2} + 6x + 7\end{equation}

What does that look like as a graph?:

```python
import pandas as pd

# Create a dataframe with an x column containing values to plot
df = pd.DataFrame ({'x': range(-8, 12)})

# Add a y column by applying the quadratic equation to x
df['y'] = -2*df['x']**2 + 6*df['x'] + 7

# Plot the line
%matplotlib inline
from matplotlib import pyplot as plt

plt.plot(df.x, df.y, color="grey")
plt.xlabel('x')
plt.ylabel('y')
plt.grid()
plt.axhline()
plt.axvline()
plt.show()
```

Again, the graph shows a parabola, but this time instead of being open at the top, the parabola is open at the bottom.

Equations that assign a value to ***y*** based on an expression that includes a squared value for ***x*** create parabolas. If the relationship between ***y*** and ***x*** is such that ***y*** is a *positive* multiple of the ***x2*** term, the parabola will be open at the top; when ***y*** is a *negative* multiple of the ***x2*** term, then the parabola will be open at the bottom.

These kinds of equations are known as *quadratic* equations, and they have some interesting characteristics. There are several ways quadratic equations can be written, but the *standard form* for quadratic equation is:

\begin{equation}y = ax^{2} + bx + c\end{equation}

Where ***a***, ***b***, and ***c*** are numeric coefficients or constants.

Let's start by examining the parabolas generated by quadratic equations in more detail.

## Parabola Vertex and Line of Symmetry
Parabolas are symmetrical, with x and y values converging exponentially towards the highest point (in the case of a downward opening parabola) or lowest point (in the case of an upward opening parabola). The point where the parabola meets the line of symmetry is known as the *vertex*.

Run the following cell to see the line of symmetry and vertex for the two parabolas described previously (don't worry about the calculations used to find the line of symmetry and vertex - we'll explore that later):

```python
%matplotlib inline

def plot_parabola(a, b, c):
    import pandas as pd
    import numpy as np
    from matplotlib import pyplot as plt

    # get the x value for the line of symmetry
    vx = (-1*b)/(2*a)

    # get the y value when x is at the line of symmetry
    vy = a*vx**2 + b*vx + c

    # Create a dataframe with an x column containing values from x-10 to x+10
    minx = int(vx - 10)
    maxx = int(vx + 11)
    df = pd.DataFrame ({'x': range(minx, maxx)})

    # Add a y column by applying the quadratic equation to x
    df['y'] = a*df['x']**2 + b *df['x'] + c

    # get min and max y values
    miny = df.y.min()
    maxy = df.y.max()

    # Plot the line
    plt.plot(df.x, df.y, color="grey")
    plt.xlabel('x')
    plt.ylabel('y')
    plt.grid()
    plt.axhline()
    plt.axvline()

    # plot the line of symmetry
    sx = [vx, vx]
    sy = [miny, maxy]
    plt.plot(sx,sy, color='magenta')

    # Annotate the vertex
    plt.scatter(vx,vy, color="red")
    plt.annotate('vertex',(vx, vy), xytext=(vx - 1, (vy + 5)* np.sign(a)))

    plt.show()

plot_parabola(2, 2, -4)

plot_parabola(-2, 3, 5)
```

## Parabola Intercepts
Recall that linear equations create lines that intersect the **x** and **y** axis of a graph, and we call the points where these intersections occur *intercepts*. Now look at the graphs of the parabolas we've worked with so far. Note that these parabolas both have a y-intercept; a point where the line intersects the y axis of the graph (in other words, when x is 0). However, note that the parabolas have *two* x-intercepts; in other words there are two points at which the line crosses the x axis (and y is 0). Additionally, imagine a downward opening parabola with its vertex at -1, -1. This is perfectly possible, and the line would never have an x value greater than -1, so it would have *no* x-intercepts.

Regardless of whether the parabola crosses the x axis or not, other than the vertex, for every ***y*** point in the parabola, there are *two* ***x*** points; one on the right (or positive) side of the axis of symmetry, and one of the left (or negative) side. The implications of this are what make quadratic equations so interesting. When we solve the equation for ***x***, there are *two* correct answers.

Let's take a look at an example to demonstrate this. Let's return to the first of our quadratic equations, and we'll look at it in its *factored* form:

\begin{equation}y = 2(x - 1)(x + 2)\end{equation}

Now, let's solve this equation for a ***y*** value of 0. We can restate the equation like this:

\begin{equation}2(x - 1)(x + 2) = 0\end{equation}

The equation is the product of two expressions **2(x - 1)** and **(x + 2)**. In this case, we know that the product of these expressions is 0, so logically *one or both of the expressions must return 0*.

Let's try the first one:

\begin{equation}2(x - 1) = 0\end{equation}

If we distrbute this, we get:

\begin{equation}2x - 2 = 0\end{equation}

This simplifies to:

\begin{equation}2x = 2\end{equation}

Which gives us a value for *x* of **1**.

Now let's try the other expression:

\begin{equation}x + 2 = 0\end{equation}

This gives us a value for *x* of **-2**.

So, when *y* is **0**, *x* is **-2** or **1**. Let's plot these points on our parabola:

```python
import pandas as pd

# Assign the calculated x values
x1 = -2
x2 = 1

# Create a dataframe with an x column containing some values to plot
df = pd.DataFrame ({'x': range(x1-5, x2+6)})

# Add a y column by applying the quadratic equation to x
df['y'] = 2*(df['x'] - 1) * (df['x'] + 2)

# Get x at the line of symmetry (halfway between x1 and x2)
vx = (x1 + x2) / 2

# Get y when x is at the line of symmetry
vy = 2*(vx -1)*(vx + 2)

# get min and max y values
miny = df.y.min()
maxy = df.y.max()

# Plot the line
%matplotlib inline
from matplotlib import pyplot as plt

plt.plot(df.x, df.y, color="grey")
plt.xlabel('x')
plt.ylabel('y')
plt.grid()
plt.axhline()
plt.axvline()

# Plot calculated x values for y = 0
plt.scatter([x1,x2],[0,0], color="green")
plt.annotate('x1',(x1, 0))
plt.annotate('x2',(x2, 0))

# plot the line of symmetry
sx = [vx, vx]
sy = [miny, maxy]
plt.plot(sx,sy, color='magenta')

# Annotate the vertex
plt.scatter(vx,vy, color="red")
plt.annotate('vertex',(vx, vy), xytext=(vx - 1, (vy - 5)))

plt.show()
```

So from the plot, we can see that both of the values we calculated for ***x*** align with the parabola when ***y*** is 0. Additionally, because the parabola is symmetrical, we know that every pair of ***x*** values for each ***y*** value will be equidistant from the line of symmetry, so we can calculate the ***x*** value for the line of symmetry as the average of the ***x*** values for any value of ***y***. This in turn means that we know the ***x*** coordinate for the vertex (it's on the line of symmetry), and we can use the quadratic equation to calculate ***y*** for this point.

## Solving Quadratics Using the Square Root Method
The technique we just looked at makes it easy to calculate the two possible values for ***x*** when ***y*** is 0 if the equation is presented as the product two expressions. If the equation is in standard form, and it can be factored, you could do the necessary manipulation to restate it as the product of two expressions. Otherwise, you can calculate the possible values for x by applying a different method that takes advantage of the relationship between squared values and the square root.

Let's consider this equation:

\begin{equation}y = 3x^{2} - 12\end{equation}

Note that this is in the standard quadratic form, but there is no *b* term; in other words, there's no term that contains a coeffecient for  ***x*** to the first power. This type of equation can be easily solved using the square root method. Let's restate it so we're solving for ***x*** when ***y*** is 0:

\begin{equation}3x^{2} - 12 = 0\end{equation}

The first thing we need to do is to isolate the ***x2*** term, so we'll remove the constant on the left by adding 12 to both sides:

\begin{equation}3x^{2} = 12\end{equation}

Then we'll divide both sides by 3 to isolate x2:

\begin{equation}x^{2} = 4\end{equation}

No we can isolate ***x*** by taking the square root of both sides. However, there's an additional consideration because this is a quadratic equation. The ***x*** variable can have two possibe values, so we must calculate the *principle* and *negative* square roots of the expression on the right:

\begin{equation}x = \pm\sqrt{4}\end{equation}

The principle square root of 4 is 2 (because 22 is 4), and the corresponding negative root is -2 (because -22 is also 4); so *x* is **2** or **-2**.

Let's see this in Python, and use the results to calculate and plot the parabola with its line of symmetry and vertex:

```python
import pandas as pd
import math

y = 0
x1 = int(- math.sqrt(y + 12 / 3))
x2 = int(math.sqrt(y + 12 / 3))

# Create a dataframe with an x column containing some values to plot
df = pd.DataFrame ({'x': range(x1-10, x2+11)})

# Add a y column by applying the quadratic equation to x
df['y'] = 3*df['x']**2 - 12

# Get x at the line of symmetry (halfway between x1 and x2)
vx = (x1 + x2) / 2

# Get y when x is at the line of symmetry
vy = 3*vx**2 - 12

# get min and max y values
miny = df.y.min()
maxy = df.y.max()

# Plot the line
%matplotlib inline
from matplotlib import pyplot as plt

plt.plot(df.x, df.y, color="grey")
plt.xlabel('x')
plt.ylabel('y')
plt.grid()
plt.axhline()
plt.axvline()

# Plot calculated x values for y = 0
plt.scatter([x1,x2],[0,0], color="green")
plt.annotate('x1',(x1, 0))
plt.annotate('x2',(x2, 0))

# plot the line of symmetry
sx = [vx, vx]
sy = [miny, maxy]
plt.plot(sx,sy, color='magenta')

# Annotate the vertex
plt.scatter(vx,vy, color="red")
plt.annotate('vertex',(vx, vy), xytext=(vx - 1, (vy - 20)))

plt.show()
```

## Solving Quadratics Using the Completing the Square Method
In quadratic equations where there is a *b* term; that is, a term containing **x** to the first power, it is impossible to directly calculate the square root. However, with some algebraic manipulation, you can take advantage of the ability to factor a polynomial expression in the form *a2 + 2ab + b2* as a binomial *perfect square* expression in the form *(a + b)2*.

At first this might seem like some sort of mathematical sleight of hand, but follow through the steps carefull and you'll see that there's nothing up my sleeve!

The underlying basis of this approach is that a trinomial expression like this:

\begin{equation}x^{2} + 24x + 12^{2}\end{equation}

Can be factored to this:

\begin{equation}(x + 12)^{2}\end{equation}

OK, so how does this help us solve a quadratic equation? Well, let's look at an example:

\begin{equation}y = x^{2} + 6x - 7\end{equation}

Let's start as we've always done so far by restating the equation to solve ***x*** for a ***y*** value of 0:

\begin{equation}x^{2} + 6x - 7 = 0\end{equation}

Now we can move the constant term to the right by adding 7 to both sides:

\begin{equation}x^{2} + 6x = 7\end{equation}

OK, now let's look at the expression on the left: *x2 + 6x*. We can't take the square root of this, but we can turn it into a trinomial that will factor into a perfect square by adding a squared constant. The question is, what should that constant be? Well, we know that we're looking for an expression like *x2 + 2**c**x + **c**2*, so our constant **c** is half of the coefficient we currently have for ***x***. This is **6**, making our constant **3**, which when squared is **9** So we can create a trinomial expression that will easily factor to a perfect square by adding 9; giving us the expression *x2 + 6x + 9*.

However, we can't just add something to one side without also adding it to the other, so our equation becomes:

\begin{equation}x^{2} + 6x + 9 = 16\end{equation}

So, how does that help? Well, we can now factor the trinomial expression as a perfect square binomial expression:

\begin{equation}(x + 3)^{2} = 16\end{equation}

And now, we can use the square root method to find x + 3:

\begin{equation}x + 3 =\pm\sqrt{16}\end{equation}

So, x + 3 is **-4** or **4**. We isolate ***x*** by subtracting 3 from both sides, so ***x*** is **-7** or **1**:

\begin{equation}x = -7, 1\end{equation}

Let's see what the parabola for this equation looks like in Python:

```python
import pandas as pd
import math

x1 = int(- math.sqrt(16) - 3)
x2 = int(math.sqrt(16) - 3)

# Create a dataframe with an x column containing some values to plot
df = pd.DataFrame ({'x': range(x1-10, x2+11)})

# Add a y column by applying the quadratic equation to x
df['y'] = ((df['x'] + 3)**2) - 16

# Get x at the line of symmetry (halfway between x1 and x2)
vx = (x1 + x2) / 2

# Get y when x is at the line of symmetry
vy = ((vx + 3)**2) - 16

# get min and max y values
miny = df.y.min()
maxy = df.y.max()

# Plot the line
%matplotlib inline
from matplotlib import pyplot as plt

plt.plot(df.x, df.y, color="grey")
plt.xlabel('x')
plt.ylabel('y')
plt.grid()
plt.axhline()
plt.axvline()

# Plot calculated x values for y = 0
plt.scatter([x1,x2],[0,0], color="green")
plt.annotate('x1',(x1, 0))
plt.annotate('x2',(x2, 0))

# plot the line of symmetry
sx = [vx, vx]
sy = [miny, maxy]
plt.plot(sx,sy, color='magenta')

# Annotate the vertex
plt.scatter(vx,vy, color="red")
plt.annotate('vertex',(vx, vy), xytext=(vx - 1, (vy - 10)))

plt.show()
```

## Vertex Form
Let's look at another example of a quadratic equation in standard form:

\begin{equation}y = 2x^{2} - 16x + 2\end{equation}

We can start to solve this by subtracting 2 from both sides to move the constant term from the right to the left:

\begin{equation}y - 2 = 2x^{2} - 16x\end{equation}

Now we can factor out the coefficient for x2, which is **2**. 2x2 is 2 • x2, and -16x is 2 • 8x:

\begin{equation}y - 2 = 2(x^{2} - 8x)\end{equation}

Now we're ready to complete the square, so we add the square of half of the -8x coefficient on the right side to the parenthesis. Half of -8 is -4, and -42 is 16, so the right side of the equation becomes *2(x2 - 8x + 16)*. Of course, we can't add something to one side of the equation without also adding it to the other side, and we've just added 2 • 16 (which is 32) to the right, so we must also add that to the left.

\begin{equation}y - 2 + 32 = 2(x^{2} - 8x + 16)\end{equation}

Now we can simplify the left and factor out a perfect square binomial expression on the right:

\begin{equation}y + 30 = 2(x - 4)^{2}\end{equation}

We now have a squared term for ***x***, so we could use the square root method to solve the equation. However, we can also isolate ***y*** by subtracting 30 from both sides. So we end up restating the original equation as:

\begin{equation}y = 2(x - 4)^{2} - 30\end{equation}

Let's just quickly check our math with Python:

```python
from random import randint
x = randint(1,100)

2*x**2 - 16*x + 2 == 2*(x - 4)**2 - 30
```

So we've managed to take the expression ***2x2 - 16x + 2*** and change it to ***2(x - 4)2 - 30***. How does that help?

Well, when a quadratic equation is stated this way, it's in *vertex form*, which is generically described as:

\begin{equation}y = a(x - h)^{2} + k\end{equation}

The neat thing about this form of the equation is that it tells us the coordinates of the vertex - it's at ***h,k***.

So in this case, we know that the vertex of our equation is 4, -30. Moreover, we know that the line of symmetry is at ***x = 4***.

We can then just use the equation to calculate two more points, and the three points will be enough for us to determine the shape of the parabola. We can simply choose any ***x*** value we like and substitute it into the equation to calculate the corresponding ***y*** value. For example, let's calculate ***y*** when x is **0**:

\begin{equation}y = 2(0 - 4)^{2} - 30\end{equation}

When we work through the equation, it gives us the answer **2**, so we know that the point 0, 2 is in our parabola.

So, we know that the line of symmetry is at ***x = h*** (which is 4), and we now know that the ***y*** value when ***x*** is 0 (***h*** - ***h***) is 2. The ***y*** value at the same distance from the line of symmetry in the negative direction will be the same as the value in the positive direction, so when ***x*** is ***h*** + ***h***, the ***y*** value will also be 2.

The following Python code encapulates all of this in a function that draws and annotates a parabola using only the ***a***, ***h***, and ***k*** values from a quadratic equation in vertex form:

```python
def plot_parabola_from_vertex_form(a, h, k):
    import pandas as pd
    import math

    # Create a dataframe with an x column a range of x values to plot
    df = pd.DataFrame ({'x': range(h-10, h+11)})

    # Add a y column by applying the quadratic equation to x
    df['y'] = (a*(df['x'] - h)**2) + k

    # get min and max y values
    miny = df.y.min()
    maxy = df.y.max()

    # calculate y when x is 0 (h+-h)
    y = a*(0 - h)**2 + k

    # Plot the line
    %matplotlib inline
    from matplotlib import pyplot as plt

    plt.plot(df.x, df.y, color="grey")
    plt.xlabel('x')
    plt.ylabel('y')
    plt.grid()
    plt.axhline()
    plt.axvline()

    # Plot calculated y values for x = 0 (h-h and h+h)
    plt.scatter([h-h, h+h],[y,y], color="green")
    plt.annotate(str(h-h) + ',' + str(y),(h-h, y))
    plt.annotate(str(h+h) + ',' + str(y),(h+h, y))

    # plot the line of symmetry (x = h)
    sx = [h, h]
    sy = [miny, maxy]
    plt.plot(sx,sy, color='magenta')

    # Annotate the vertex (h,k)
    plt.scatter(h,k, color="red")
    plt.annotate('v=' + str(h) + ',' + str(k),(h, k), xytext=(h - 1, (k - 10)))

    plt.show()

# Call the function for the example discussed above
plot_parabola_from_vertex_form(2, 4, -30)
```

It's important to note that the vertex form specifically requires a *subtraction* operation in the factored perfect square term. For example, consider the following equation in the standard form:

\begin{equation}y = 3x^{2} + 6x + 2\end{equation}

The steps to solve this are:
1. Move the constant to the left side:
\begin{equation}y - 2 = 3x^{2} + 6x\end{equation}
2. Factor the ***x*** expressions on the right:
\begin{equation}y - 2 = 3(x^{2} + 2x)\end{equation}
3. Add the square of half the x coefficient to the right, and the corresponding multiple on the left:
\begin{equation}y - 2 + 3 = 3(x^{2} + 2x + 1)\end{equation}
4. Factor out a perfect square binomial:
\begin{equation}y + 1 = 3(x + 1)^{2}\end{equation}
5. Move the constant back to the right side:
\begin{equation}y = 3(x + 1)^{2} - 1\end{equation}

To express this in vertex form, we need to convert the addition in the parenthesis to a subtraction:

\begin{equation}y = 3(x - -1)^{2} - 1\end{equation}

Now, we can use the a, h, and k values to define a parabola:

```python
plot_parabola_from_vertex_form(3, -1, -1)
```

## Shortcuts for Solving Quadratic Equations
We've spent some time in this notebook discussing how to solve quadratic equations to determine the vertex of a parabola and the ***x*** values in relation to ***y***. It's important to understand the techniques we've used, which incude:
- Factoring
- Calculating the Square Root
- Completing the Square
- Using the vertex form of the equation

The underlying algebra for all of these techniques is the same, and this consistent algebra results in some shortcuts that you can memorize to make it easier to solve quadratic equations without going through all of the steps:

### Calculating the Vertex from Standard Form
You've already seen that converting a quadratic equation to the vertex form makes it easy to identify the vertex coordinates, as they're encoded as ***h*** and ***k*** in the equation itself - like this:

\begin{equation}y = a(x - \textbf{h})^{2} + \textbf{k}\end{equation}

However, what if you have an equation in standard form?:

\begin{equation}y = ax^{2} + bx + c\end{equation}

There's a quick and easy technique you can apply to get the vertex coordinates.

1. To find ***h*** (which is the x-coordinate of the vertex), apply the following formula:
\begin{equation}h = \frac{-b}{2a}\end{equation}
2. After you've found ***h***, use it in the quadratic equation to solve for ***k***:
\begin{equation}\textbf{k} = a\textbf{h}^{2} + b\textbf{h} + c\end{equation}

For example, here's the quadratic equation in standard form that we previously converted to the vertex form:

\begin{equation}y = 2x^{2} - 16x + 2\end{equation}

To find ***h***, we perform the following calculation:

\begin{equation}h = \frac{-b}{2a}\;\;\;\;=\;\;\;\;\frac{-1 \cdot16}{2\cdot2}\;\;\;\;=\;\;\;\;\frac{16}{4}\;\;\;\;=\;\;\;\;4\end{equation}

Then we simply plug the value we've obtained for ***h*** into the quadratic equation in order to find ***k***:

\begin{equation}k = 2\cdot(4^{2}) - 16\cdot4 + 2\;\;\;\;=\;\;\;\;32 - 64 + 2\;\;\;\;=\;\;\;\;-30\end{equation}

Note that a vertex at 4,-30 is also what we previously calculated for the vertex form of the same equation:

\begin{equation}y = 2(x - 4)^{2} - 30\end{equation}

### The Quadratic Formula
Another useful formula to remember is the *quadratic formula*, which makes it easy to calculate values for ***x*** when ***y*** is **0**; or in other words:

\begin{equation}ax^{2} + bx + c = 0\end{equation}

Here's the formula:

\begin{equation}x = \frac{-b \pm \sqrt{b^{2} - 4ac}}{2a}\end{equation}

Let's apply that formula to our equation, which you may remember looks like this:

\begin{equation}y = 2x^{2} - 16x + 2\end{equation}

OK, let's plug the ***a***, ***b***, and ***c*** variables from our equation into the quadratic formula:

\begin{equation}x = \frac{--16 \pm \sqrt{-16^{2} - 4\cdot2\cdot2}}{2\cdot2}\end{equation}

This simplifes to:

\begin{equation}x = \frac{16 \pm \sqrt{256 - 16}}{4}\end{equation}

This in turn (with the help of a calculator) simplifies to:

\begin{equation}x = \frac{16 \pm 15.491933384829668}{4}\end{equation}

So our positive value for ***x*** is:

\begin{equation}x = \frac{16 + 15.491933384829668}{4}\;\;\;\;=7.872983346207417\end{equation}

And the negative value for ***x*** is:

\begin{equation}x = \frac{16 - 15.491933384829668}{4}\;\;\;\;=0.12701665379258298\end{equation}

The following Python code uses the vertex formula and the quadtratic formula to calculate the vertex and the -x and +x for y = 0, and then plots the resulting parabola:

```python
def plot_parabola_from_formula (a, b, c):
    import math

    # Get vertex
    print('CALCULATING THE VERTEX')
    print('vx = -b / 2a')

    nb = -b
    a2 = 2*a
    print('vx = ' + str(nb) + ' / ' + str(a2))

    vx = -b/(2*a)
    print('vx = ' + str(vx))

    print('\nvy = ax^2 + bx + c')
    print('vy =' + str(a) + '(' + str(vx) + '^2) + ' + str(b) + '(' + str(vx) + ') + ' + str(c))

    avx2 = a*vx**2
    bvx = b*vx
    print('vy =' + str(avx2) + ' + ' + str(bvx) + ' + ' + str(c))

    vy = avx2 + bvx + c
    print('vy = ' + str(vy))

    print ('\nv = ' + str(vx) + ',' + str(vy))

    # Get +x and -x (showing intermediate calculations)
    print('\nCALCULATING -x AND +x FOR y=0')
    print('x = -b +- sqrt(b^2 - 4ac) / 2a')

    b2 = b**2
    ac4 = 4*a*c
    print('x = ' + str(nb) + '+-sqrt(' + str(b2) + ' - ' + str(ac4) + ')/' + str(a2))

    sr = math.sqrt(b2 - ac4)
    print('x = ' + str(nb) + ' +- ' + str(sr) + ' / ' + str(a2))
    print('-x = ' + str(nb) + ' - ' + str(sr) + ' / ' + str(a2))
    print('+x = ' + str(nb) + ' + ' + str(sr) + ' / ' + str(a2))

    posx = (nb + sr) / a2
    negx = (nb - sr) / a2
    print('-x = ' + str(negx))
    print('+x = ' + str(posx))

    print('\nPLOTTING THE PARABOLA')
    import pandas as pd

    # Create a dataframe with an x column a range of x values to plot
    df = pd.DataFrame ({'x': range(round(vx)-10, round(vx)+11)})

    # Add a y column by applying the quadratic equation to x
    df['y'] = a*df['x']**2 + b*df['x'] + c

    # get min and max y values
    miny = df.y.min()
    maxy = df.y.max()

    # Plot the line
    %matplotlib inline
    from matplotlib import pyplot as plt

    plt.plot(df.x, df.y, color="grey")
    plt.xlabel('x')
    plt.ylabel('y')
    plt.grid()
    plt.axhline()
    plt.axvline()

    # Plot calculated x values for y = 0
    plt.scatter([negx, posx],[0,0], color="green")
    plt.annotate('-x=' + str(negx) + ',' + str(0),(negx, 0), xytext=(negx - 3, 5))
    plt.annotate('+x=' + str(posx) + ',' + str(0),(posx, 0), xytext=(posx - 3, -10))

    # plot the line of symmetry
    sx = [vx, vx]
    sy = [miny, maxy]
    plt.plot(sx,sy, color='magenta')

    # Annotate the vertex
    plt.scatter(vx,vy, color="red")
    plt.annotate('v=' + str(vx) + ',' + str(vy),(vx, vy), xytext=(vx - 1, vy - 10))

    plt.show()

plot_parabola_from_formula (2, -16, 2)
```

Basics Of Algebra by Hiren/01-07-Quadratic Equations.ipynb


# First Order Initial Value Problem

The more general form of a first order Ordinary Differential Equation is:
\begin{equation}
\label{general ODE}
y^{'}=f(t,y).
\end{equation}
This can be solved analytically by integrating both sides but this is not straight forward for most problems.
Numerical methods can be used to approximate the solution at discrete points.

## Euler method

The simplest one step numerical method is the Euler Method named after the most prolific of mathematicians [Leonhard Euler](https://en.wikipedia.org/wiki/Leonhard_Euler) (15 April 1707 – 18 September 1783) .

The general Euler formula to the first order equation
$$ y^{'} = f(t,y) $$
approximates the derivative at time point $t_i$
$$y^{'}(t_i) \approx \frac{w_{i+1}-w_i}{t_{i+1}-t_{i}} $$
where $w_i$ is the approximate solution of $y$ at time $t_i$.
This substitution changes the differential equation  into a __difference__ equation of the form
$$
\frac{w_{i+1}-w_i}{t_{i+1}-t_{i}}=f(t_i,w_i) $$
Assuming uniform stepsize $t_{i+1}-t_{i}$ is replaced by $h$, re-arranging the equation gives
$$ w_{i+1}=w_i+hf(t_i,w_i),$$
 This can be read as the future $w_{i+1}$ can be approximated by the present $w_i$ and the addition of the input to the system $f(t,y)$ times the time step.

```python
## Library
import numpy as np
import math

%matplotlib inline
import matplotlib.pyplot as plt # side-stepping mpl backend
import matplotlib.gridspec as gridspec # subplots
import warnings

warnings.filterwarnings("ignore")

```

## Population growth

The general form of the population growth differential equation is:
$$ y^{'}=\epsilon y $$
where $\epsilon$ is the growth rate. The initial population at time $a$ is
$$ y(a)=A $$
$$ a\leq t \leq b. $$
Integrating gives  the general analytic (exact) solution:
$$ y=Ae^{\epsilon x}. $$
We will use this equation to illustrate the application of the Euler method.

## Discrete Interval
The continuous time $a\leq t \leq b $ is discretised into $N$ points seperated by a constant stepsize
$$ h=\frac{b-a}{N}.$$
Here the interval is $0\leq t \leq 2$
$$ h=\frac{2-0}{20}=0.1.$$
This gives the 21 discrete points:
$$ t_0=0, \ t_1=0.1, \ ... t_{20}=2. $$
This is generalised to
$$ t_i=0+i0.1, \ \ \ i=0,1,...,20.$$
The plot below shows the discrete time steps.

```python
### Setting up time
t_end=2.0
t_start=0
N=20
h=(t_end-t_start)/(N)
time=np.arange(t_start,t_end+0.01,h)
fig = plt.figure(figsize=(10,4))
plt.plot(time,0*time,'o:',color='red')
plt.xlim((0,2))
plt.title('Illustration of discrete time points for h=%s'%(h))
```

## Initial Condition
To get a specify solution to a first order initial value problem, an __initial condition__ is required.

For our population problem the intial condition is:
$$y(0)=10$$.
This gives the analytic solution
$$y=10e^{\epsilon t}.$$
### Growth rate
Let the growth rate $$\epsilon=0.5$$ giving the analytic solution.
$$y=10e^{0.5 t}.$$
The plot below shows the exact solution on the discrete time steps.

```python
## Analytic Solution y
y=10*np.exp(0.5*time)

fig = plt.figure(figsize=(10,4))
plt.plot(time,y,'o:',color='black')
plt.xlim((0,2))
plt.xlabel('time')
plt.ylabel('y')
plt.title('Analytic (Exact) solution')
```

## Numerical approximation of Population growth
The differential equation is transformed using the Euler method into a difference equation of the form
      $$ w_{i+1}=w_{i}+h \epsilon w_i. $$
This approximates a series of of values $w_0, \ w_1, \ ..., w_{N}$.
For the specific example of the population equation the difference equation is
      $$ w_{i+1}=w_{i}+h 0.5 w_i. $$
where $w_0=10$. From this initial condition the series is approximated.
The plot below shows the exact solution $y$ in black circles and Euler approximation $w$ in blue squares.

```python
w=np.zeros(N+1)
w[0]=10
for i in range (0,N):
    w[i+1]=w[i]+h*(0.5)*w[i]

fig = plt.figure(figsize=(10,4))
plt.plot(time,y,'o:',color='black',label='exact')
plt.plot(time,w,'s:',color='blue',label='Euler')
plt.xlim((0,2))
plt.xlabel('time')
plt.legend(loc='best')
plt.title('Analytic and Euler solution')
```

## Error
With a numerical solution there are two types of error:
* local truncation error at one time step;
* global error which is the propagation of local error.

### Derivation of  Euler Local truncation error
The left hand side of a initial value problem $\frac{dy}{dt}$ is approximated by __Taylors theorem__ expand about a point $t_0$ giving:
\begin{equation}y(t_1) = y(t_0)+(t_1-t_0)y^{'}(t_0) + \frac{(t_1-t_0)^2}{2!}y^{''}(\xi), \ \ \ \ \ \ \xi \in [t_0,t_1]. \end{equation}
Rearranging and letting $h=t_1-t_0$ the equation becomes
$$y^{'}(t_0)=\frac{y(t_1)-y(t_0)}{h}-\frac{h}{2}y^{''}(\xi). $$
From this the local truncation error is
$$\tau y^{'}(t_0)\leq \frac{h}{2}M  $$
where $y^{''}(t) \leq M $.
#### Derivation of  Euler Local truncation error for the Population Growth
In most cases $y$ is unknown but in our example problem there is an exact solution which can be used to estimate the local truncation
$$y'(t)=5e^{0.5 t}$$
$$y''(t)=2.5e^{0.5 t}$$
From this a maximum upper limit can be calculated for $y^{''} $ on the interval $[t_0,t_1]=[0,0.1]$
$$y''(0.1)=2.5e^{0.1\times 0.5}=2.63=M$$
$$\tau=\frac{h}{2}2.63=0.1315 $$
The plot below shows the exact local truncation error $|y-w|$ (red triangle) and the upper limit of the Truncation error (black v) for the first two time points $t_0$ and $t_1$.

```python
fig = plt.figure(figsize=(10,4))
plt.plot(time[0:2],np.abs(w[0:2]-y[0:2]),'^:'
         ,color='red',label='Error |y-w|')
plt.plot(time[0:2],0.1*2.63/2*np.ones(2),'v:'
         ,color='black',label='Upper Local Truncation')
plt.xlim((0,.15))
plt.xlabel('time')
plt.legend(loc='best')
plt.title('Local Truncation Error')
```

## Global Error
The error does not stay constant accross the time this is illustrated in the figure below for the population growth equation. The actual error (red triangles) increases over time while the local truncation error (black v) remains constant.

```python
fig = plt.figure(figsize=(10,4))
plt.plot(time,np.abs(w-y),'^:'
         ,color='red',label='Error |y-w|')
plt.plot(time,0.1*2.63/2*np.ones(N+1),'v:'
         ,color='black',label='Upper Local Truncation')
plt.xlim((0,2))
plt.xlabel('time')
plt.legend(loc='best')
plt.title('Why Local Truncation does not extend to global')
```

## Theorems
To theorem below proves an upper limit of the global truncation error.
### Euler Global Error
__Theorem Global Error__

Suppose $f$ is continuous and satisfies a Lipschitz Condition with constant
L on $D=\{(t,y)|a\leq t \leq b, -\infty < y < \infty \}$ and that a constant M
exists with the property that
$$ |y^{''}(t)|\leq M. $$
Let $y(t)$ denote the unique solution of the Initial Value Problem
$$ y^{'}=f(t,y) \ \ \ a\leq t \leq b \ \ \ y(a)=\alpha $$
and $w_0,w_1,...,w_N$ be the approx generated by the Euler method for some
positive integer N.  Then for $i=0,1,...,N$
$$ |y(t_i)-w_i| \leq \frac{Mh}{2L}|e^{L(t_i-a)}-1|. $$

### Theorems about Ordinary Differential Equations
__Definition__

A function $f(t,y)$ is said to satisfy a __Lipschitz Condition__ in the variable $y$ on
the set $D \subset R^2$ if a constant $L>0$ exist with the property that
$$ |f(t,y_1)-f(t,y_2)| < L|y_1-y_2| $$
whenever $(t,y_1),(t,y_2) \in D$.  The constant L is call the Lipschitz Condition
of $f$.

__Theorem__
Suppose $f(t,y)$ is defined on a convex set $D \subset R^2$. If a constant
$L>0$ exists with
$$ \left|\frac{\partial f(t,y)}{\partial y}\right|\leq L $$
then $f$ satisfies a Lipschitz Condition an $D$ in the variable $y$ with
Lipschitz constant L.

### Global truncation error for the population equation
For the population equation specific values $L$ and $M$ can be calculated.

In this case $f(t,y)=\epsilon y$ is continuous and satisfies a Lipschitz Condition with constant
$$ \left|\frac{\partial f(t,y)}{\partial y}\right|\leq L $$
$$ \left|\frac{\partial \epsilon y}{\partial y}\right|\leq \epsilon=0.5=L $$

on $D=\{(t,y)|0\leq t \leq 2, 10 < y < 30 \}$ and that a constant $M$
exists with the property that
$$ |y^{''}(t)|\leq M. $$
$$ |y^{''}(t)|=2.5e^{0.5\times 2} \leq 2.5 e=6.8. $$
Let $y(t)$ denote the unique solution of the Initial Value Problem
$$ y^{'}=0.5 y \ \ \ 0\leq t \leq 10 \ \ \ y(0)=10 $$
and $w_0,w_1,...,w_N$ be the approx generated by the Euler method for some
positive integer N.  Then for $i=0,1,...,N$
$$ |y(t_i)-w_i| \leq \frac{6.8 h}{2\times 0.5}|e^{0.5(t_i-0)}-1| $$

The figure below shows the exact error $y-w$ in red triangles and the upper global error in black x's.

```python
fig = plt.figure(figsize=(10,4))
plt.plot(time,np.abs(w-y),'^:'
         ,color='red',label='Error |y-w|')
plt.plot(time,0.1*6.8*(np.exp(0.5*time)-1),'x:'
         ,color='black',label='Upper Global Truncation')
plt.xlim((0,2))
plt.xlabel('time')
plt.legend(loc='best')
plt.title('Global Truncation Error')
```

```python

```

Chapter 01 - Euler Methods/.ipynb_checkpoints/01_Euler_method_with_Theorems_Growth_function-checkpoint.ipynb


# Solve a nonlinear system
\begin{align}
3x_1 - \cos(x_2x_3) - \frac{1}{2}& = 0\\
x_1^2 - 81(x_2+0.1)^2 + \sin(x_3) +1.06& = 0\\
e^{-x_1x_2} + 20 x_3 + \frac{10\pi-3}{3}& = 0
\end{align}

```julia
using Printf
using ForwardDiff
using LinearAlgebra
using NLsolve
```

## Hand Coded Jacobian

```julia
function F(x)
    return [3*x[1] - cos(x[2]*x[3]) - 0.5
            x[1]^2 - 81 * (x[2]+0.1)^2 + sin(x[3])+1.06
            exp(-x[1]*x[2]) + 20 * x[3] + (10 * π-3)/3];
end

function J(x)
    J_ = zeros(3,3);
    J_[1,1] = 3;
    J_[1,2] = x[3] * sin(x[2]*x[3]);
    J_[1,3] = x[2] * sin(x[2]*x[3]);

    J_[2,1] = 2*x[1];
    J_[2,2] =-162 * (x[2]+0.1);
    J_[2,3] = cos(x[3]);

    J_[3,1] = -x[2] * exp(-x[1]*x[2]);
    J_[3,2] = -x[1] * exp(-x[1]*x[2])
    J_[3,3] = 20;
    return J_
end
```

```julia
F(zeros(3))
```

```julia
J([0., 0., 0.])
```

```julia
# starting guess
x = zeros(3);
δ = zeros(3);
n_iters = 20;
tol = 1e-10;

for n in 1:n_iters
    δ .= -J(x)\F(x);
    @. x += δ;
    f_err = norm(F(x));
    @printf("%d: ||F(x)|| = %g\n",n, f_err);

    if(f_err<tol)
        break;
    end
end
@show x;

```

## Automatic Differentiation

```julia
J_auto = x-> ForwardDiff.jacobian(F, x);
```

```julia
J_auto(x)
```

```julia
# starting guess
x = zeros(3);
δ = zeros(3);
n_iters = 20;
tol = 1e-10;

for n in 1:n_iters
    δ .= -J_auto(x)\F(x);
    @. x += δ;
    f_err = norm(F(x));
    @printf("%d: ||F(x)|| = %g\n",n, f_err);

    if(f_err<tol)
        break;
    end
end
@show x;

```

## Solution with NLsolve

```julia
x = zeros(3);
soln= nlsolve(F, x)
```

```julia
x = zeros(3);
soln= nlsolve(F,J_auto, x)
```


# Chapter 10 - Position and Momentum
We can start using sympy to handle symbolic math (integrals and other calculus):

```python
from sympy import *
```

```python
init_printing(use_unicode=True)
```

```python
x, y, z = symbols('x y z', real=True)
a, c = symbols('a c', nonzero=True, real=True)
```

```python
integrate?
```

There are two ways to use the `integrate` function. In one line, like `integrate(x,(x,0,1))` or by naming an expression and then integrating it over a range:

```
A = (c*cos((pi*x)/(2.0*a)))**2
A.integrate((x,-a,a),conds='none')
```

We'll use both, at different times. For longer expressions, the second form can be easier to read and write.

First, just try the following, then we'll re-create some examples in the book.

```python
integrate(x,(x,0,1))
```

```python
integrate(x**2,(x,0,1))
```

The cell below will return an odd set of conditions on the result. This is because the solver doesn't want to assume anything about `a` and there is a special case where the answer would be different. If you look closely though, that special case isn't physically realistic so to igore these special conditions, we add `conds='none'`. The next cell down does what you'd expect. From here on out, just add this to the `integrate` function and we'll get what we expect.

```python
A = (c*cos((pi*x)/(2.0*a)))**2
A.integrate((x,-a,a))
```

```python
A = (c*cos((pi*x)/(2.0*a)))**2
A.integrate((x,-a,a), conds='none')
```

So this tells us the normalization constant should be $c=\frac{1}{\sqrt{a}}$. Check that it is normalized if we do that:

```python
psi = 1/sqrt(a)*cos((pi*x)/(2.0*a))  # notice we can name the expression something useful.
B = psi**2
B.integrate( (x,-a,a), conds='none')
```

Because `psi` is a real function, we can calculate expectation values by integrating over $x$ or $x^2$ with `psi**2`:

```python
C = x*psi**2
C.integrate( (x,-a,a), conds='none')
```

```python
D = x**2 * psi**2
E = D.integrate( (x,-a,a), conds='none')
```

```python
E.n()  # the .n() method approximates the numerical part. You can look at the full expression below.
```

```python
E
```

## Example 10.2

```python
h = Symbol('hbar', real=True)
```

Use the `diff` function to take a derivative of a symbolic expression. For example:

```python
diff(x**2, x)
```

```python
# Solution goes here
```

```python
# Solution goes here
```

## Example 10.3

```python
p = Symbol('p', real=True)
```

```python
# Solution goes here
```

```python
# Solution goes here
```

```python

```

Chapter 10 - Position & Momentum_blank.ipynb


## Linear Algebra

```python
%matplotlib inline
```

```python
import numpy as np
import matplotlib.pyplot as plt

import sympy
```

```python
def plt_complex(numbers, colors, xlim = (-5, 5),ylim = (-5, 5)):
    plt.quiver(
        [0]*len(numbers),
        [0]*len(numbers),
        [number.real for number in numbers],
        [number.imag for number in numbers],
        scale = 1,
        scale_units = "xy",
        angles = "xy",
        color = colors)

    plt.gca().set_aspect("equal")
    plt.grid()
    plt.xlim(xlim)
    plt.ylim(ylim)
    plt.show()
```

```python
plt_complex([2+3j, 3 - 2j, 4], ["red", "blue", "grey"])
```

```python
x =sympy.symbols("x")
```

```python
sympy.solve((x-1)**2, x)
```

    [1]

```python
solutions = sympy.solve((x-3)*(x-5)+40+x**3)
```

```python
sol = [complex(solution) for solution in solutions]
```

```python
plt_complex(sol, ["red", "blue"], (-8, 8), (-8,8))
```

```python
sol = sympy.solve(x**36+1)
solutions = [complex(solution) for solution in sol]
plt_complex(solutions, ["red", "blue", "green"], (-1.3, 1.3), (-1.3,1.3))
```

## Vectors
#### scalar product

```python
np.dot([2,3], [1,5])
```

    17

```python
scalar = np.dot([2,3,0],[1,5,0])
vector = np.cross([2,3,0],[1,5,0])

print(scalar)
print(vector)
```

    17
    [0 0 7]

```python
np.dot(np.random.randint(2,20,10), np.random.randint(2,20,10)) # 10 dimentional vectors
```

    485

```python
A = np.array([
    [2,3,7],
    [8,9,1]
])
B = np.array([
    [1,-3,0],
    [2,-4,1]
])
```

```python
A*B   #Adamar multiplication
```

    array([[  2,  -9,   0],
           [ 16, -36,   1]])

```python
A @B.T    #Vectors multiplication
```

    array([[ -7,  -1],
           [-19, -19]])


# Part 3.2

## Python OOP 2 with Keynesian Samuelson Accelerator Example

This lecture creates nonstochastic and stochastic versions of Paul Samuelson’s celebrated multiplier accelerator model [Sam39]

In doing so, we extend the example of the Solow model class in our second OOP lecture

Our objectives are to

* provide a more detailed example of OOP and classes
* review a famous model
* review linear difference equations, both deterministic and stochastic

### Samuelson’s Model

Samuelson used a second-order linear difference equation to represent a model of national output based on three components:

* a national output identity asserting that national outcome is the sum of consumption plus investment plus government purchases
* a Keynesian consumption function asserting that consumption at time $t$ is equal to a constant times national output at time $t-1$ an investment accelerator asserting that investment at time $t$
equals a constant called the accelerator coefficient times the difference in output between period $t-1$ and $t-2$

* the idea that consumption plus investment plus government purchases constitute aggregate demand, which automatically calls forth an equal amount of aggregate supply

* *(To read about linear difference equations see [Linear Diff Eq's on Wikipedia](https://en.wikipedia.org/wiki/Linear_difference_equation) or chapter IX of [Sar87](https://lectures.quantecon.org/py/zreferences.html#sargent1987))*

Samuelson used the model to analyze how particular values of the marginal propensity to consume and the accelerator coefficient might give rise to transient business cycles in national output

Possible dynamic properties include:

* smooth convergence to a constant level of output
* damped business cycles that eventually converge to a constant level of output
* persistent business cycles that neither dampen nor explode

Later we present an extension that adds a random shock to the right side of the national income identity representing random fluctuations in aggregate demand

This modification makes national output become governed by a second-order stochastic linear difference equation that, with appropriate parameter values, gives rise to recurrent irregular business cycles.

(To read about stochastic linear difference equations see chapter XI of [Sar87](https://lectures.quantecon.org/py/zreferences.html#sargent1987))

#### Details

Let’s assume that

* ${G_t}$ is a sequence of levels of government expenditures. We’ll start by setting **$G_t=G$** *for all $t$*
* ${C_t}$ is a sequence of levels of aggregate consumption expenditures, a key **endogenous** variable in the model
* ${I_t}$ is a sequence of rates of investment, another key **endogenous** variable
* ${Y_t}$ is a sequence of levels of national income, yet another **endogenous** variable
* $a$ is the marginal propensity to consume in the Keynesian consumption function $C_t = a Y_{t-1} + \gamma$
* $b$ is the “accelerator coefficient” in the **“investment accelerator”** $I_t = b (Y_{t-1} - Y_{t-2})$
* ${ε_t}$ is an IID sequence standard normal random variables.
* $σ≥0$ is a “volatility” parameter — setting $σ=0$ recovers the nonstochastic case that we’ll start with.

The model combines the consumption function $C_t = a Y_{t-1} + \gamma$ $(1)$ with the investment accelerator $I_t = b (Y_{t-1} - Y_{t-2})$ $(2)$ and the national income identity $Y_t=C_t+I_t+G_t$ $(3)$

* The parameter $a$ is peoples’ marginal propensity to consume out of income - equation $(1)$ asserts that people consume a fraction of `math:a in (0,1)` of each additional dollar of income
* The parameter $b>0$ is the investment accelerator coefficient - equation $(2)$ asserts that people invest in physical capital when income is increasing and disinvest when it is decreasing

Equations $(1), (2), and (3)$ imply the following second-order linear difference equation for national income:

* $Y_t = (a+b) Y_{t-1} - b Y_{t-2} + (\gamma + G_t)$

or

* $Y_t = \rho_1 Y_{t-1} + \rho_2 Y_{t-2} + (\gamma + G_t)$    $(4)$

where $ρ_1=(a+b)$ and $ρ_2=−b$

To complete the model, we require two initial conditions

If the model is to generate time series for $t=0, \ldots, T$, we require initial values,

* $Y_{-1} = \bar Y_{-1}, \quad  Y_{-2} = \bar Y_{-2}$

We’ll ordinarily set the parameters $(a,b)$ so that starting from an arbitrary pair  of initial conditions $(\bar Y_{-1}, \bar Y_{-2})$, national income $Y_t$ converges to a constant value as $t$ becomes large.

We are interested in studying

* the transient fluctuations in $Y_t$ as it converges to its **steady state** level
* the **rate** at which it converges to a steady state level

The deterministic version of the model described so far — meaning that no random shocks hit aggregate demand — has only transient fluctuations.

We can convert the model to one that has persistent irregular fluctuations by adding a random shock to aggregate demand

##### Stochastic version of the model

We create a **random** or **stochastic** version of the model by adding a random process of **shocks** or **disturbances** $\{\sigma \epsilon_t \}$ to the right side of equation $(4)$, leading to the second-order scalar linear stochastic difference equation:

* $Y_t = G_t + a (1-b) Y_{t-1} - a b Y_{t-2} + \sigma \epsilon_{t}$    $(5)$

##### Mathematical analysis of the model

To get started, let’s set $G_t≡0$, $σ=0$, and $γ=0$.

Then we can write equation $(5)$ as

* $Y_t = \rho_1 Y_{t-1} + \rho_2 Y_{t-2}$

or

* $Y_{t+2} - \rho_1 Y_{t+1} - \rho_2 Y_t  = 0$ $(6)$

To discover the properties of the solution of $(6)$, it is useful first to form the characteristic polynomial for $(6)$:

* $z^2−ρ_1z−ρ_2$    $(7)$

where $z$ is possibly a complex number

We want to find the two zeros (a.k.a. roots) – namely $λ_1,λ_2$ – of the characteristic polynomial.

These are two special values of $z$, say $z=λ_1$ and $z=λ_2$, such that if we set $z$ equal to one of these values in expression $(7)$, the characteristic polynomial $(7)$ equals zero:

* $z^2−ρ_1z−ρ_2=(z−λ_1)(z−λ_2)=0$    $(8)$

Equation $(8)$ is said to **factor** the characteristic polynomial

When the roots are complex, they will occur as a *complex conjugate pair*

When the roots are complex, it is convenient to represent them in the polar form

* $λ_1=re^{iω}, λ2=re^{−iω}$

where $r$ is the amplitude of the complex number and $ω$ is its angle or phase

These can also be represented as

* $λ_1=r(cos(ω)+isin(ω))\\ λ_2=r(cos(ω)−isin(ω))$

Given initial conditions $Y_{−1},Y_{−2}$, we want to generate a solution of the difference equation $(6)$

It can be represented as
* $Y_t = λ_1^t c_1 + λ_2^t c_2$

where $c1$ and $c2$ are constants that depend on the two initial conditions and on $ρ_1,ρ_2$

When the roots are complex, it is useful to pursue the following calculations

Notice that:

* $$
\begin{eqnarray}
Y_t & =  & c_1 (r e^{i ω})^t + c_2 (r e^{-i ω})^t  \\
& = & c_1 r^t e^{iω t} + c_2 r^t e^{-i ω t} \\
& = &  c_1 r^t [\cos(ω t) + i \sin(ω t) ] + c_2 r^t [\cos(ω t) - i \sin(ω t) ] \\
& = & (c_1 + c_2) r^t \cos(ω t) + i (c_1 - c_2) r^t \sin(ω t)
\end{eqnarray}
$$

The only way that $Y_t$
can be a real number for each t is if $c_1+c_2$ is a real number and $c_1−c_2$ is an imaginary number This happens only when $c_1$ and $c_2$ are complex conjugates, in which case they can be written in the polar forms $c_1=ve^{iθ},  c_2=ve^{−iθ}$

So we can write

* $$
\begin{eqnarray}
Y_t & = & v e^{i \theta} r^t e^{i \omega t} + v e ^{- i \theta} r^t e^{-i \omega t} \\
& = & v r^t [ e^{i(\omega t + \theta)} + e^{-i (\omega t +\theta)}]  \\
& = & 2 v r^t  \cos (\omega t + \theta)
\end{eqnarray}
$$

where $v$ and $θ$ are constants that must be chosen to satisfy initial conditions for $Y_1,Y_2$

where $~c_1, ~c_2$ is a pair of constants chosen to satisfy the given initial conditions for $Y_1,Y_2$

This formula shows that when the roots are complex, $Y_t$ displays oscillations with period $\check p =
\frac{2π}{ω}$ and damping factor $r$ We say that $\check p$ is the period because in that amount of time the cosine wave $cos(ωt+θ)$

goes through exactly one complete cycles (Draw a cosine funtion to convince yourself of this please)

Remark: Following [Sam39], we want to choose the parameters $a,b$
of the model so that the absolute values (of the possibly complex) roots $λ_1,λ_2$ of the characteristic polynomial are both strictly less than one:

* $|λ_j|<1 \text{ for } j=1,2$

Remark: When both roots $λ_1,λ_2$ of the characteristic polynomial have absolute values strictly less than one, the absolute value of the larger one governs the rate of convergence to the steady state of the non stochastic version of the model

##### Things this lecture does

We write a function to generate simulations of a ${Y_t}$ sequence as a function of time

The function requires that we put in initial conditions for $Y_1,Y_2$

The function checks that $a,b$ are set so that $λ_1,λ_2$ are less than unity in absolute value (also called “modulus”)

The function also tells us whether the roots are complex, and, if they are complex, returns both their real and complex parts

If the roots are both real, the function returns their values We use our function written to simulate paths that are stochastic (when $σ>0$)

We have written the function in a way that allows us to input ${G_t}$ paths of a few simple forms, e.g.,

* one time jumps in G at some time
* a permanent jump in G that occurs at some time

We proceed to use the Samuelson multiplier-accererator model as a laboratory to make a simple OOP example

The “state” that determines next period’s $Y_{t+1}$ is now not just the current value $Y_t$ but also the once lagged value $Y_{t−1}$

This involves a little more bookkeeping than is required in the Solow model class definition

**We use the Samuelson multiplier-accelerator model as a vehicle for teaching how we can gradually add more features to the class.**

We want to have a method in the class that automatically generates a simulation, either nonstochastic $(σ=0)$ or stochastic $(σ>0)$

We also show how to map the Samuelson model into a simple instance of the `LinearStateSpace` class described here

We can use a `LinearStateSpace` instance to do various things that we did above with our homemade function and class

Among other things, we show by example that the eigenvalues of the matrix $A$

that we use to form the instance of the LinearStateSpace class for the Samuelson model equal the roots of the characteristic polynomial $(7)$ for the Samuelson multiplier accelerator model

Here is the formula for the matrix $A$
in the linear state space system in the case that government expenditures are a constant $G$

* $A = \begin{bmatrix} 1 & 0 & 0 \cr \gamma + G & \rho_1 & \rho_2 \cr 0 & 1 & 0 \end{bmatrix}$

##### Implementation

We’ll start by drawing an informative graph from page 189 of [Sar87]

```python
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline

def param_plot():

    """this function creates the graph on page 189 of Sargent Macroeconomic Theory, second edition, 1987"""

    fig, ax = plt.subplots(figsize=(12, 8))
    ax.set_aspect('equal')

    # Set axis
    xmin, ymin = -3, -2
    xmax, ymax = -xmin, -ymin
    plt.axis([xmin, xmax, ymin, ymax])

    # Set axis labels
    ax.set(xticks=[], yticks=[])
    ax.set_xlabel(r'$\rho_2$', fontsize=16)
    ax.xaxis.set_label_position('top')
    ax.set_ylabel(r'$\rho_1$', rotation=0, fontsize=16)
    ax.yaxis.set_label_position('right')

    # Draw (t1, t2) points
    ρ1 = np.linspace(-2, 2, 100)
    ax.plot(ρ1, -abs(ρ1) + 1, c='black')
    ax.plot(ρ1, np.ones_like(ρ1) * -1, c='black')
    ax.plot(ρ1, -(ρ1**2 / 4), c='black')

    # Turn normal axes off
    for spine in ['left', 'bottom', 'top', 'right']:
        ax.spines[spine].set_visible(False)

    # Add arrows to represent axes
    axes_arrows = {'arrowstyle': '<|-|>', 'lw': 1.3}
    ax.annotate('', xy=(xmin, 0), xytext=(xmax, 0), arrowprops=axes_arrows)
    ax.annotate('', xy=(0, ymin), xytext=(0, ymax), arrowprops=axes_arrows)

    # Annotate the plot with equations
    plot_arrowsl = {'arrowstyle': '-|>', 'connectionstyle': "arc3, rad=-0.2"}
    plot_arrowsr = {'arrowstyle': '-|>', 'connectionstyle': "arc3, rad=0.2"}
    ax.annotate(r'$\rho_1 + \rho_2 < 1$', xy=(0.5, 0.3), xytext=(0.8, 0.6),
                arrowprops=plot_arrowsr, fontsize='12')
    ax.annotate(r'$\rho_1 + \rho_2 = 1$', xy=(0.38, 0.6), xytext=(0.6, 0.8),
                arrowprops=plot_arrowsr, fontsize='12')
    ax.annotate(r'$\rho_2 < 1 + \rho_1$', xy=(-0.5, 0.3), xytext=(-1.3, 0.6),
                arrowprops=plot_arrowsl, fontsize='12')
    ax.annotate(r'$\rho_2 = 1 + \rho_1$', xy=(-0.38, 0.6), xytext=(-1, 0.8),
                arrowprops=plot_arrowsl, fontsize='12')
    ax.annotate(r'$\rho_2 = -1$', xy=(1.5, -1), xytext=(1.8, -1.3),
                arrowprops=plot_arrowsl, fontsize='12')
    ax.annotate(r'${\rho_1}^2 + 4\rho_2 = 0$', xy=(1.15, -0.35),
                xytext=(1.5, -0.3), arrowprops=plot_arrowsr, fontsize='12')
    ax.annotate(r'${\rho_1}^2 + 4\rho_2 < 0$', xy=(1.4, -0.7),
                xytext=(1.8, -0.6), arrowprops=plot_arrowsr, fontsize='12')

    # Label categories of solutions
    ax.text(1.5, 1, 'Explosive\n growth', ha='center', fontsize=16)
    ax.text(-1.5, 1, 'Explosive\n oscillations', ha='center', fontsize=16)
    ax.text(0.05, -1.5, 'Explosive oscillations', ha='center', fontsize=16)
    ax.text(0.09, -0.5, 'Damped oscillations', ha='center', fontsize=16)

    # Add small marker to y-axis
    ax.axhline(y=1.005, xmin=0.495, xmax=0.505, c='black')
    ax.text(-0.12, -1.12, '-1', fontsize=10)
    ax.text(-0.12, 0.98, '1', fontsize=10)

    return fig

param_plot()
plt.show()
```

The graph portrays regions in which the $(λ_1,λ_2)$ root pairs implied by the $(ρ_1=(a+b),ρ_2=−b)$ difference equation parameter pairs in the Samuelson model are such that:

* $(λ_1,λ_2)$ are complex with modulus less than 1 - in this case, the ${Y_t}$ sequence displays damped oscillations
* $(λ_1,λ_2)$ are both real, but one is strictly greater than 1 - this leads to explosive growth
* $(λ_1,λ_2)$ are both real, but one is strictly less than −1 - this leads to explosive oscillations
* $(λ_1,λ_2)$ are both real and both are less than 1 in absolute value - in this case, there is smooth convergence to the steady state without damped cycles

Later we’ll present the graph with a red mark showing the particular point implied by the setting of $(a,b)$

##### **Function to describe implications of characteristic polynomial**

```python
def categorize_solution(ρ1, ρ2):
    """this function takes values of ρ1 and ρ2 and uses them to classify the type of solution"""

    discriminant = ρ1 ** 2 + 4 * ρ2
    if ρ2 > 1 + ρ1 or ρ2 < -1:
        print('Explosive oscillations')
    elif ρ1 + ρ2 > 1:
        print('Explosive growth')
    elif discriminant < 0:
        print('Roots are complex with modulus less than one; therefore damped oscillations')
    else:
        print('Roots are real and absolute values are less than zero; therefore get smooth convergence to a steady state')
```

```python
### Test the categorize_solution function

categorize_solution(1.3, -.4)
```

    Roots are real and absolute values are less than zero; therefore get smooth convergence to a steady state

##### **Function for plotting $Y_t$ paths**

* A useful function for our work below

```python
def plot_y(function=None):
    """function plots path of Y_t"""
    plt.subplots(figsize=(12, 8))
    plt.plot(function)
    plt.xlabel('Time $t$')
    plt.ylabel('$Y_t$', rotation=0)
    plt.grid()
    plt.show()
```

#### **Manual or “by hand” root calculations**

The following function calculates roots of the characteristic polynomial using high school algebra

(We’ll calculate the roots in other ways later)

The function also plots a $Y_t$ starting from initial conditions that we set

```python
from cmath import sqrt

##=== This is a 'manual' method ===#

def y_nonstochastic(y_0=100, y_1=80, α=.92, β=.5, γ=10, n=80):

    """Takes values of parameters and computes roots of characteristic polynomial.
       It tells whether they are real or complex and whether they are less than unity in absolute value.
       It also computes a simulation of length n starting from the two given initial conditions for national income"""

    roots = []

    ρ1 = α + β
    ρ2 = -β

    print(f'ρ_1 is {ρ1}')
    print(f'ρ_2 is {ρ2}')

    discriminant = ρ1 ** 2 + 4 * ρ2

    if discriminant == 0:
        roots.append(-ρ1 / 2)
        print('Single real root: ')
        print(''.join(str(roots)))
    elif discriminant > 0:
        roots.append((-ρ1 + sqrt(discriminant).real) / 2)
        roots.append((-ρ1 - sqrt(discriminant).real) / 2)
        print('Two real roots: ')
        print(''.join(str(roots)))
    else:
        roots.append((-ρ1 + sqrt(discriminant)) / 2)
        roots.append((-ρ1 - sqrt(discriminant)) / 2)
        print('Two complex roots: ')
        print(''.join(str(roots)))

    if all(abs(root) < 1 for root in roots):
        print('Absolute values of roots are less than one')
    else:
        print('Absolute values of roots are not less than one')

    def transition(x, t): return ρ1 * x[t - 1] + ρ2 * x[t - 2] + γ

    y_t = [y_0, y_1]

    for t in range(2, n):
        y_t.append(transition(y_t, t))

    return y_t

plot_y(y_nonstochastic())

```

##### **Reverse engineering parameters to generate damped cycles**

The next cell writes code that takes as inputs the modulus $r$ and phase $ϕ$ of a conjugate pair of complex numbers in polar form
$λ_1=r \space exp(iϕ),\space λ_2=r \space exp(−iϕ)$

* The code assumes that these two complex numbers are the roots of the characteristic polynomial
* It then reverse engineers $(a,b)$ and $(ρ_1,ρ_2)$, pairs that would generate those roots

```python
### code to reverse engineer a  cycle
### y_t = r^t (c_1 cos(ϕ t) + c2 sin(ϕ t))
###

import cmath
import math

def f(r, ϕ):
    """
    Takes modulus r and angle ϕ of complex number r exp(j ϕ)
    and creates ρ1 and ρ2 of characteristic polynomial for which
    r exp(j ϕ) and r exp(- j ϕ) are complex roots.

    Returns the multiplier coefficient a and the accelerator coefficient b
    that verifies those roots.
    """
    g1 = cmath.rect(r, ϕ)  # Generate two complex roots
    g2 = cmath.rect(r, -ϕ)
    ρ1 = g1 + g2           # Implied ρ1, ρ2
    ρ2 = -g1 * g2
    b = -ρ2                # Reverse engineer a and b that validate these
    a = ρ1 - b
    return ρ1, ρ2, a, b

## Now let's use the function in an example
## Here are the example paramters

r = .95
period = 10                # Length of cycle in units of time
ϕ = 2 * math.pi/period

## Apply the function

ρ1, ρ2, a, b = f(r, ϕ)

print(f"a, b = {a}, {b}")
print(f"ρ1, ρ2 = {ρ1}, {ρ2}")
```

    a, b = (0.6346322893124001+0j), (0.9024999999999999-0j)
    ρ1, ρ2 = (1.5371322893124+0j), (-0.9024999999999999+0j)

```python

## Print the real components of ρ1 and ρ2

ρ1 = ρ1.real
ρ2 = ρ2.real

ρ1, ρ2

```

    (1.5371322893124, -0.9024999999999999)

#### **Root finding using numpy**

Here we’ll use numpy to compute the roots of the characteristic polynomial

```python
r1, r2 = np.roots([1, -ρ1, -ρ2])

p1 = cmath.polar(r1)
p2 = cmath.polar(r2)

print(f"r, ϕ = {r}, {ϕ}")
print(f"p1, p2 = {p1}, {p2}")
# print(f"g1, g2 = {g1}, {g2}")

print(f"a, b = {a}, {b}")
print(f"ρ1, ρ2 = {ρ1}, {ρ2}")
```

    r, ϕ = 0.95, 0.6283185307179586
    p1, p2 = (0.95, 0.6283185307179586), (0.95, -0.6283185307179586)
    a, b = (0.6346322893124001+0j), (0.9024999999999999-0j)
    ρ1, ρ2 = 1.5371322893124, -0.9024999999999999

```python
##=== This method uses numpy to calculate roots ===#

def y_nonstochastic(y_0=100, y_1=80, α=.9, β=.8, γ=10, n=80):

    """ Rather than computing the roots of the characteristic polynomial by hand as we did earlier, this function
    enlists numpy to do the work for us """

    # Useful constants
    ρ1 = α + β
    ρ2 = -β

    categorize_solution(ρ1, ρ2)

    # Find roots of polynomial
    roots = np.roots([1, -ρ1, -ρ2])
    print(f'Roots are {roots}')

    # Check if real or complex
    if all(isinstance(root, complex) for root in roots):
        print('Roots are complex')
    else:
        print('Roots are real')

    # Check if roots are less than one
    if all(abs(root) < 1 for root in roots):
        print('Roots are less than one')
    else:
        print('Roots are not less than one')

    # Define transition equation
    def transition(x, t): return ρ1 * x[t - 1] + ρ2 * x[t - 2] + γ

    # Set initial conditions
    y_t = [y_0, y_1]

    # Generate y_t series
    for t in range(2, n):
        y_t.append(transition(y_t, t))

    return y_t

plot_y(y_nonstochastic())
```

##### **Reverse engineered complex roots: example**

The next cell studies the implications of reverse engineered complex roots

We’ll generate an **undamped** cycle of period **10**

```python
r = 1   # generates undamped, nonexplosive cycles

period = 10   #  length of cycle in units of time
ϕ = 2 * math.pi/period

## Apply the reverse engineering function f

ρ1, ρ2, a, b = f(r, ϕ)

a = a.real  # drop the imaginary part so that it is a valid input into y_nonstochastic
b = b.real

print(f"a, b = {a}, {b}")

ytemp = y_nonstochastic(α=a, β=b, y_0=20, y_1=30)
plot_y(ytemp)
```

##### **Digression**: *using sympy to find roots*

We can also use sympy to compute analytic formulas for the roots

```python
import sympy
from sympy import Symbol, init_printing
init_printing()

r1 = Symbol("ρ_1")
r2 = Symbol("ρ_2")
z = Symbol("z")

sympy.solve(z**2 - r1*z - r2, z)
```

```python
a = Symbol("α")
b = Symbol("β")
r1 = a + b
r2 = -b

sympy.solve(z**2 - r1*z - r2, z)
```

##### **Stochastic shocks**

Now we’ll construct some code to simulate the stochastic version of the model that emerges when we add a random shock process to aggregate demand

```python
def y_stochastic(y_0=0, y_1=0, α=0.8, β=0.2, γ=10, n=100, σ=5):

    """This function takes parameters of a stochastic version of the model and proceeds to analyze
    the roots of the characteristic polynomial and also generate a simulation"""

    # Useful constants
    ρ1 = α + β
    ρ2 = -β

    # Categorize solution
    categorize_solution(ρ1, ρ2)

    # Find roots of polynomial
    roots = np.roots([1, -ρ1, -ρ2])
    print(roots)

    # Check if real or complex
    if all(isinstance(root, complex) for root in roots):
        print('Roots are complex')
    else:
        print('Roots are real')

    # Check if roots are less than one
    if all(abs(root) < 1 for root in roots):
        print('Roots are less than one')
    else:
        print('Roots are not less than one')

    # Generate shocks
    ϵ = np.random.normal(0, 1, n)

    # Define transition equation
    def transition(x, t): return ρ1 * \
        x[t - 1] + ρ2 * x[t - 2] + γ + σ * ϵ[t]

    # Set initial conditions
    y_t = [y_0, y_1]

    # Generate y_t series
    for t in range(2, n):
        y_t.append(transition(y_t, t))

    return y_t

plot_y(y_stochastic())
```

Let’s do a simulation in which there are shocks and the characteristic polynomial has complex roots

```python
r = .97

period = 10   #  length of cycle in units of time
ϕ = 2 * math.pi/period

### apply the  reverse engineering function f

ρ1, ρ2, a, b = f(r, ϕ)

a = a.real  # drop the imaginary part so that it is a valid input into y_nonstochastic
b = b.real

print(f"a, b = {a}, {b}")
plot_y(y_stochastic(y_0=40, y_1 = 42, α=a, β=b, σ=2, n=100))
```

##### **Government spending**

This function computes a response to either a permanent or one-off increase in government expenditures

```python
def y_stochastic_g(y_0=20,
                   y_1=20,
                   α=0.8,
                   β=0.2,
                   γ=10,
                   n=100,
                   σ=2,
                   g=0,
                   g_t=0,
                   duration='permanent'):

    """This program computes a response to a permanent increase in government expenditures that occurs
       at time 20"""

    # Useful constants
    ρ1 = α + β
    ρ2 = -β

    # Categorize solution
    categorize_solution(ρ1, ρ2)

    # Find roots of polynomial
    roots = np.roots([1, -ρ1, -ρ2])
    print(roots)

    # Check if real or complex
    if all(isinstance(root, complex) for root in roots):
        print('Roots are complex')
    else:
        print('Roots are real')

    # Check if roots are less than one
    if all(abs(root) < 1 for root in roots):
        print('Roots are less than one')
    else:
        print('Roots are not less than one')

    # Generate shocks
    ϵ = np.random.normal(0, 1, n)

    def transition(x, t, g):

        # Non-stochastic - separated to avoid generating random series when not needed
        if σ == 0:
            return ρ1 * x[t - 1] + ρ2 * x[t - 2] + γ + g

        # Stochastic
        else:
            ϵ = np.random.normal(0, 1, n)
            return ρ1 * x[t - 1] + ρ2 * x[t - 2] + γ + g + σ * ϵ[t]

    # Create list and set initial conditions
    y_t = [y_0, y_1]

    # Generate y_t series
    for t in range(2, n):

        # No government spending
        if g == 0:
            y_t.append(transition(y_t, t))

        # Government spending (no shock)
        elif g != 0 and duration == None:
            y_t.append(transition(y_t, t))

        # Permanent government spending shock
        elif duration == 'permanent':
            if t < g_t:
                y_t.append(transition(y_t, t, g=0))
            else:
                y_t.append(transition(y_t, t, g=g))

        # One-off government spending shock
        elif duration == 'one-off':
            if t == g_t:
                y_t.append(transition(y_t, t, g=g))
            else:
                y_t.append(transition(y_t, t, g=0))
    return y_t

```

A permanent government spending shock can be simulated as follows

```python

plot_y(y_stochastic_g(g=10, g_t=20, duration='permanent'))
```

We can also see the response to a one time jump in government expenditures

```python
plot_y(y_stochastic_g(g=500, g_t=50, duration='one-off'))
```

#### **Wrapping everything into a class**

Up to now we have written functions to do the work

Now we’ll roll up our sleeves and write a Python class called `Samuelson` for the Samuleson model

```python
class Samuelson():

    r"""This class represents the Samuelson model, otherwise known as the
    multiple-accelerator model. The model combines the Keynesian multiplier
    with the accelerator theory of investment.

    The path of output is governed by a linear second-order difference equation

    .. math::

        Y_t =  + \alpha (1 + \beta) Y_{t-1} - \alpha \beta Y_{t-2}

    Parameters
    ----------
    y_0 : scalar
        Initial condition for Y_0
    y_1 : scalar
        Initial condition for Y_1
    α : scalar
        Marginal propensity to consume
    β : scalar
        Accelerator coefficient
    n : int
        Number of iterations
    σ : scalar
        Volatility parameter. Must be greater than or equal to 0. Set
        equal to 0 for non-stochastic model.
    g : scalar
        Government spending shock
    g_t : int
        Time at which government spending shock occurs. Must be specified
        when duration != None.
    duration : {None, 'permanent', 'one-off'}
        Specifies type of government spending shock. If none, government
        spending equal to g for all t.

    """

    def __init__(self,
                 y_0=100,
                 y_1=50,
                 α=1.3,
                 β=0.2,
                 γ=10,
                 n=100,
                 σ=0,
                 g=0,
                 g_t=0,
                 duration=None):

        self.y_0, self.y_1, self.α, self.β = y_0, y_1, α, β
        self.n, self.g, self.g_t, self.duration = n, g, g_t, duration
        self.γ, self.σ = γ, σ
        self.ρ1 = α + β
        self.ρ2 = -β
        self.roots = np.roots([1, -self.ρ1, -self.ρ2])

    def root_type(self):
        if all(isinstance(root, complex) for root in self.roots):
            return 'Complex conjugate'
        elif len(self.roots) > 1:
            return 'Double real'
        else:
            return 'Single real'

    def root_less_than_one(self):
        if all(abs(root) < 1 for root in self.roots):
            return True

    def solution_type(self):
        ρ1, ρ2 = self.ρ1, self.ρ2
        discriminant = ρ1 ** 2 + 4 * ρ2
        if ρ2 >= 1 + ρ1 or ρ2 <= -1:
            return 'Explosive oscillations'
        elif ρ1 + ρ2 >= 1:
            return 'Explosive growth'
        elif discriminant < 0:
            return 'Damped oscillations'
        else:
            return 'Steady state'

    def _transition(self, x, t, g):

        # Non-stochastic - separated to avoid generating random series when not needed
        if self.σ == 0:
            return self.ρ1 * x[t - 1] + self.ρ2 * x[t - 2] + self.γ + g

        # Stochastic
        else:
            ϵ = np.random.normal(0, 1, self.n)
            return self.ρ1 * x[t - 1] + self.ρ2 * x[t - 2] + self.γ + g + self.σ * ϵ[t]

    def generate_series(self):

        # Create list and set initial conditions
        y_t = [self.y_0, self.y_1]

        # Generate y_t series
        for t in range(2, self.n):

            # No government spending
            if self.g == 0:
                y_t.append(self._transition(y_t, t))

            # Government spending (no shock)
            elif self.g != 0 and self.duration == None:
                y_t.append(self._transition(y_t, t))

            # Permanent government spending shock
            elif self.duration == 'permanent':
                if t < self.g_t:
                    y_t.append(self._transition(y_t, t, g=0))
                else:
                    y_t.append(self._transition(y_t, t, g=self.g))

            # One-off government spending shock
            elif self.duration == 'one-off':
                if t == self.g_t:
                    y_t.append(self._transition(y_t, t, g=self.g))
                else:
                    y_t.append(self._transition(y_t, t, g=0))
        return y_t

    def summary(self):
        print('Summary\n' + '-' * 50)
        print(f'Root type: {self.root_type()}')
        print(f'Solution type: {self.solution_type()}')
        print(f'Roots: {str(self.roots)}')

        if self.root_less_than_one() == True:
            print('Absolute value of roots is less than one')
        else:
            print('Absolute value of roots is not less than one')

        if self.σ > 0:
            print('Stochastic series with σ = ' + str(self.σ))
        else:
            print('Non-stochastic series')

        if self.g != 0:
            print('Government spending equal to ' + str(self.g))

        if self.duration != None:
            print(self.duration.capitalize() +
                  ' government spending shock at t = ' + str(self.g_t))

    def plot(self):
        fig, ax = plt.subplots(figsize=(12, 8))
        ax.plot(self.generate_series())
        ax.set(xlabel='Iteration', xlim=(0, self.n))
        ax.set_ylabel('$Y_t$', rotation=0)
        ax.grid()

        # Add parameter values to plot
        paramstr = f'$\\alpha={self.α:.2f}$ \n $\\beta={self.β:.2f}$ \n $\\gamma={self.γ:.2f}$ \n \
$\\sigma={self.σ:.2f}$ \n $\\rho_1={self.ρ1:.2f}$ \n $\\rho_2={self.ρ2:.2f}$'
        props = dict(fc='white', pad=10, alpha=0.5)
        ax.text(0.87, 0.05, paramstr, transform=ax.transAxes,
                fontsize=12, bbox=props, va='bottom')

        return fig

    def param_plot(self):

        # Uses the param_plot() function defined earlier (it is then able
        # to be used standalone or as part of the model)

        fig = param_plot()
        ax = fig.gca()

        # Add λ values to legend
        for i, root in enumerate(self.roots):
            if isinstance(root, complex):
                operator = ['+', '']  # Need to fill operator for positive as string is split apart
                label = rf'$\lambda_{i+1} = {sam.roots[i].real:.2f} {operator[i]} {sam.roots[i].imag:.2f}i$'
            else:
                label = rf'$\lambda_{i+1} = {sam.roots[i].real:.2f}$'
            ax.scatter(0, 0, 0, label=label) # dummy to add to legend

        # Add ρ pair to plot
        ax.scatter(self.ρ1, self.ρ2, 100, 'red', '+', label=r'$(\ \rho_1, \ \rho_2 \ )$', zorder=5)

        plt.legend(fontsize=12, loc=3)

        return fig

```

#### **Illustration of Samuelson class**

Now we’ll put our Samuelson class to work on an example

```python
sam = Samuelson(α=0.8, β=0.5, σ=2, g=10, g_t=20, duration='permanent')
sam.summary()
```

    Summary
    --------------------------------------------------
    Root type: Complex conjugate
    Solution type: Damped oscillations
    Roots: [0.65+0.27838822j 0.65-0.27838822j]
    Absolute value of roots is less than one
    Stochastic series with σ = 2
    Government spending equal to 10
    Permanent government spending shock at t = 20

```python
sam.plot()
plt.show()
```

#### Using the graph

We’ll use our graph to show where the roots lie and how their location is consistent with the behavior of the path just graphed

The red $+$ sign shows the location of the roots

```python
sam.param_plot()
plt.show()
```

#### **Using the LinearStateSpace class**

It turns out that we can use the QuantEcon.py LinearStateSpace class to do much of the work that we have done from scratch above

Here is how we map the Samuelson model into an instance of a `LinearStateSpace` class

```python
from quantecon import LinearStateSpace

""" This script maps the Samuelson model in the the LinearStateSpace class"""
α = 0.8
β = 0.9
ρ1 = α + β
ρ2 = -β
γ = 10
σ = 1
g = 10
n = 100

A = [[1,        0,      0],
     [γ + g,   ρ1,     ρ2],
     [0,        1,      0]]

G = [[γ + g, ρ1,   ρ2],         # this is Y_{t+1}
     [γ,      α,    0],         # this is C_{t+1}
     [0,      β,   -β]]         # this is I_{t+1}

μ_0 = [1, 100, 100]
C = np.zeros((3,1))
C[1] = σ # stochastic

sam_t = LinearStateSpace(A, C, G, mu_0=μ_0)

x, y = sam_t.simulate(ts_length=n)

fig, axes = plt.subplots(3, 1, sharex=True, figsize=(15, 8))
titles = ['Output ($Y_t$)', 'Consumption ($C_t$)', 'Investment ($I_t$)']
colors = ['darkblue', 'red', 'purple']
for ax, series, title, color in zip(axes, y, titles, colors):
    ax.plot(series, color=color)
    ax.set(title=title, xlim=(0, n))
    ax.grid()

axes[-1].set_xlabel('Iteration')

plt.show()

```

#### **Other methods in the LinearStateSpace class**

Let’s plot **impulse response functions** for the instance of the Samuelson model using a method in the `LinearStateSpace` class

```python
imres = sam_t.impulse_response()
imres = np.asarray(imres)
y1 = imres[:, :, 0]
y2 = imres[:, :, 1]
y1.shape
```

Now let’s compute the zeros of the characteristic polynomial by simply calculating the eigenvalues of $A$

```python
A = np.asarray(A)
w, v = np.linalg.eig(A)
print(w)
```

    [0.85+0.42130749j 0.85-0.42130749j 1.  +0.j        ]

##### **Inheriting methods from** `LinearStateSpace`

We could also create a subclass of `LinearStateSpace` (inheriting all its methods and attributes) to add more functions to use

```python
class SamuelsonLSS(LinearStateSpace):

    """
    this subclass creates a Samuelson multiplier-accelerator model
    as a linear state space system
    """
    def __init__(self,
                 y_0=100,
                 y_1=100,
                 α=0.8,
                 β=0.9,
                 γ=10,
                 σ=1,
                 g=10):

        self.α, self.β = α, β
        self.y_0, self.y_1, self.g = y_0, y_1, g
        self.γ, self.σ = γ, σ

        # Define intial conditions
        self.μ_0 = [1, y_0, y_1]

        self.ρ1 = α + β
        self.ρ2 = -β

        # Define transition matrix
        self.A = [[1,                 0,         0],
                  [γ + g,       self.ρ1,   self.ρ2],
                  [0,                 1,         0]]

        # Define output matrix
        self.G = [[γ + g, self.ρ1, self.ρ2],         # this is Y_{t+1}
                  [γ,           α,       0],         # this is C_{t+1}
                  [0,           β,      -β]]         # this is I_{t+1}

        self.C = np.zeros((3, 1))
        self.C[1] = σ  # stochastic

        # Initialize LSS with parameters from Samuleson model
        LinearStateSpace.__init__(self, self.A, self.C, self.G, mu_0=self.μ_0)

    def plot_simulation(self, ts_length=100, stationary=True):

        # Temporarily store original parameters
        temp_μ = self.μ_0
        temp_Σ = self.Sigma_0

        # Set distribution parameters equal to their stationary values for simulation
        if stationary == True:
            try:
                self.μ_x, self.μ_y, self.σ_x, self.σ_y = self.stationary_distributions()
                self.μ_0 = self.μ_y
                self.Σ_0 = self.σ_y
            # Exception where no convergence achieved when calculating stationary distributions
            except ValueError:
                print('Stationary distribution does not exist')

        x, y = self.simulate(ts_length)

        fig, axes = plt.subplots(3, 1, sharex=True, figsize=(15, 8))
        titles = ['Output ($Y_t$)', 'Consumption ($C_t$)', 'Investment ($I_t$)']
        colors = ['darkblue', 'red', 'purple']
        for ax, series, title, color in zip(axes, y, titles, colors):
            ax.plot(series, color=color)
            ax.set(title=title, xlim=(0, n))
            ax.grid()

        axes[-1].set_xlabel('Iteration')

        # Reset distribution parameters to their initial values
        self.μ_0 = temp_μ
        self.Sigma_0 = temp_Σ

        return fig

    def plot_irf(self, j=5):

        x, y = self.impulse_response(j)

        # Reshape into 3 x j matrix for plotting purposes
        yimf = np.array(y).flatten().reshape(j+1, 3).T

        fig, axes = plt.subplots(3, 1, sharex=True, figsize=(15, 8))
        labels = ['$Y_t$', '$C_t$', '$I_t$']
        colors = ['darkblue', 'red', 'purple']
        for ax, series, label, color in zip(axes, yimf, labels, colors):
            ax.plot(series, color=color)
            ax.set(xlim=(0, j))
            ax.set_ylabel(label, rotation=0, fontsize=14, labelpad=10)
            ax.grid()

        axes[0].set_title('Impulse Response Functions')
        axes[-1].set_xlabel('Iteration')

        return fig

    def multipliers(self, j=5):
        x, y = self.impulse_response(j)
        return np.sum(np.array(y).flatten().reshape(j+1, 3), axis=0)
```

##### **Illustrations**

Let’s show how we can use the `SamuelsonLSS`

```python
samlss = SamuelsonLSS()
samlss.plot_simulation(100, stationary=False)
plt.show()
```

```python
samlss.plot_simulation(100, stationary=True)
plt.show()
```

```python
samlss.plot_irf(100)
plt.show()
```

```python
samlss.multipliers()
```

    array([7.414389, 6.835896, 0.578493])

##### **Pure multiplier model**

Let’s shut down the accelerator by setting $b=0$ to get a pure multiplier model

* the absence of cycles gives an idea about why Samuelson included the accelerator

```python
pure_multiplier = SamuelsonLSS(α=0.95, β=0)
pure_multiplier.plot_simulation()
```

```python
pure_multiplier = SamuelsonLSS(α=0.8, β=0)
pure_multiplier.plot_simulation()
```

```python
pure_multiplier.plot_irf(100)
```


```python
%pylab inline
%config InlineBackend.figure_format = 'retina'
from ipywidgets import interact
```

    Populating the interactive namespace from numpy and matplotlib

# Question 1
Consider the approximation to the first derivative
$$ f'(a) \approx  \frac{f(a+h) - f(a )}{h}.$$
The discretization error for this formula is $O(h)$. Suppose that the absolute error in evaluating the function $f$ is bounded by machine epsilon, $\epsilon$, with
$$ \vert \hat{f}(x) - f(x) \vert \leq \epsilon, $$
where $\hat{f}(x)$ is the floating point approximation of the exact function evaluation $f(x)$. For simplicity, let us ignore the errors generated in basic arithmetic operations.   Suppose further that there is a constant $M>0$ such that $\vert f’’(x)\vert \leq M$ for all $x$.

**Ignore the floating point error in approximating $h$ with a floating point value in the denominator.**

## A.
Show that the total computational error (truncation and rounding combined) is bounded by
$$ \frac{Mh}{2} + \frac{2\epsilon}{h}.$$

---------------------------------------
## Solution
The first part is the discretization error we derived in Homework 2. The second part,
$$ \mathcal{E}_{\rm roundoff} = \frac{2\epsilon}{h},$$
is due to roundoff error. The second term follows from the bound on error in evaluating the function. Using the triangle inequality, we have
\begin{align}
\frac{\vert f({\rm fl}(a+h)) - f({\rm fl}(a) ) -  (f(a+h) - f(a ))\vert}{h}
 & \leq \frac{\vert f({\rm fl}(a+h)) - f(a+h)\vert  + \vert f({\rm fl}(a) ) -  f(a )\vert )\vert}{h} \\
 & \leq \frac{\epsilon + \epsilon}{h} = \frac{2\epsilon}{h}.
\end{align}

## B.
Treat the above bound as an approximation of the absolute error. At what value of $h$ is the error estimate in part A minimized?

---------------------------------------
## Solution
Take the derivative of the total error with respect to $h$, set it to zero, and solve for $h_{\rm opt}$,
$$ \frac{M}{2} - \frac{2\epsilon}{h_{\rm opt}^2} = 0,$$
which yields
$$h_{\rm opt} = \sqrt{\frac{4\epsilon}{M}} .$$

## C.
Copy and paste Example 2 in the Week 2 Python notebook into a new notebook. Plot the error estimate derived in part A (again treating the bound as an approximation) along with the empirical computational error (the numerically computed error) and the estimate of the discretization error (both of these are already plotted in the example). Is the error estimate from part A a better error estimate? Why? Plot the minimum predicted in part B. Does it agree with the minimum of the empirical error? **For the graph, use $\epsilon = 10^{-16}$ and $M = |f''(a)|$.**

```python
x0 = 1.2 ## point that we compute the derivative at (ie d/dx sin(x) at x = x0)
f0 = sin(x0) ## f(x0)
fp = cos(x0) ## f'(x0) the `p` means 'prime'
fpp = -sin(x0) ## f''(x0)

i = linspace(-20, 0, 40) ## `linspace` gives a range of values between two end points
##                          in this case 40 points, between -20 and 0
h = 10.0**i ## this is our approx parameter, it is an array of values
##             between 10^(-20) and 10^(0)
fp_approx = (sin(x0 + h) - f0)/h ## the derivative approximation
err = absolute(fp - fp_approx) ## the full absolute error
d_err = h/2*absolute(fpp) ## the formula for the discretization error, derived above

### New code here
machine_epsilon = np.finfo(float).eps # 2.2e-16
epsilon = 1e-16
M = absolute(fpp)
h_opt = sqrt(4.*epsilon/M)
err_opt = M*h_opt/2. + 2.*epsilon/h_opt
err_tot = M*h/2. + 2.*epsilon/h
## we can get a slightly improved error estimate if we account for the fact
## that h = epsilon_machine ~ 2e-16 is the lowest possible value that can
## be achieved
err_max_from_underflow = 2.*epsilon/machine_epsilon
err_tot_clipped = clip(err_tot, -inf, err_max_from_underflow)
####################

figure(1, [10, 5]) ## creates a blank figure 7 inches (wide) by 5 inches (height)
loglog(h, err, '-*', label='empirical error') ## makes a plot with a double log scale
loglog(h, d_err, 'r-', label='estimate:\ndiscretization error')

### New code here
loglog(h_opt, err_opt, 'og', ms=15, label='optimal h')
loglog(h, err_tot_clipped, 'g', label='estimate:\ntotal error')
####################

### some new code below too, but just plotting stuff
xlabel('h', fontsize=20) ## puts a label on the x axis
ylabel('absolute error', fontsize=20) ## puts a label on the y axis
xlim(1e-25, 1e0)
ylim(1e-13, 2) ## places limits on the yaxis for our plot
legend(loc='lower left', fontsize=18); ## creates a figure legend (uses the `label=...`
                                       ## arguments in the plot command)
```

# Question 2
Suppose we invent a new representation for finite precision real numbers based on rational numbers instead of floating point numbers. Rational numbers are dense on the reals, and we can approximate any real number to any desired precision with a rational number. For this to work, our data structure must use only integers and arithmetic operations ($+,-,\cdot, /$) on integers.

Let $x = I_1 / I_2$ where $I_1$ and $I_2$ are 16bit integers. Assume for simplicity that $0 \leq I_1 \leq I_{\rm max}$ and $0 < I_2 \leq I_{\rm max}$. Hence, each real number represented in finite precision with our new system uses 32bits to store in memory.

## A
Devise formulas to perform addition, multiplication, and division that use only arithmetic operations on integers. Arithmetic operations should take as input two numbers in our format and return a single new number also in our format. For example, if $x_1 = I_{11}/I_{21}$ and $x_2 = I_{12}/I_{22}$, then $x_1 + x_2 = x_3$, where $x_3$ is expressed as the ratio of two integers. For each operation, you need to write $x_3$ as the ratio of two integers each of which are functions of the integers $I_{11},I_{21}, I_{12}, I_{22}$.

---------------------------------------
## Solution
Assume the output of a given operation is $y = y_1/y_2$ for integers $y_1$ and $y_2$.
  1. Multiplication $x_1x_2$:
  $$ y_{1} = I_{11}I_{12},\quad y_2 = I_{21}I_{22} $$
  2. Division $x_1/x_2$:
  $$y_{1} = I_{11}I_{22},\quad y_2 = I_{21}I_{12}$$
  3. Addition and subtraction $x_1 \pm x_2$:
  $$ y_{1} = I_{11}I_{22} \pm I_{12}I_{21}, \quad y_{2} = I_{21}I_{22}$$

## B
What is $I_{max}$ for a non negative 16bit integer?

---------------------------------------
## Solution
$$ 2^{16} - 1 = 65,535 $$

## C
What is the smallest possible nonzero value that can be represented by our numbers (remember that we are assuming they are non negative)?

---------------------------------------
## Solution
$$\approx 2^{-16} \approx 10^{-5}$$

## D
What is the largest possible value that can be represented by our numbers?

---------------------------------------
## Solution
$$\approx 2^{16} \approx 10^5$$

## E
What is the smallest (in absolute value) possible absolute difference between two numbers $x_1$ and $x_2$ such that $x_1 \neq x_2$?

---------------------------------------
## Solution
Not shown

## F
What is the smallest (in absolute value) possible relative difference between two numbers $x_1$ and $x_2$ such that $x_1 \neq x_2$? Use the following for relative difference $$\frac{|x_1 - x_2| }{ \max(x_1, x_2)}$$

---------------------------------------
## Solution
Not shown

---------------------------------------
## Solution

## G
How do the above answers compare to 32bit floating point numbers? Is this a good way to represent real numbers on a computer? Why or why not?

---------------------------------------
## Solution
Our numbers based on rational numbers are not nearly as good as 32 bit floating point numbers.
  1. The range of values is much smaller. The larges values are $\approx 10^5$ compared to $\approx 10^{38}$ for 32bit float. The smallest values follow a similar trend.
  2. The accuracy, or machine epsilon, is much larger
  3. Several students pointed out that the floating point operations would likely be much slower


# Estimation of pi using Monte Carlo method

**Statement of problem**

Let two random variables $X$ and $Y$ be in $[-1,1]$ and distributed uniformly. Probability that point given with coordinates $(X, Y)$ lies in unit circle (i.e. $X^2 + Y^2 < 1$) is:

\begin{equation}
p = \frac{S_C}{S_S} = \frac{r^2 \pi}{a^2} = \frac{\pi}{4}
\end{equation}

**Method**

Generate $N$ pairs of $(X, Y)$ and calculate how many of them lay in unit circle, $N_<$. Then estimate probability that point is in circle by:

\begin{equation}
p_< = \frac{N_<}{N}
\end{equation}

By law of large numbers, this number will converge to $p$ as $N \to \infty$. Hence, our estimation of $\pi$ is:

\begin{equation}
\hat \pi = 4p_<
\end{equation}

**Tasks**

1. Generate $N$ pairs of $(X, Y)$, calculate for each $N$ how many of them are in unit circle $N<$. Choose $N$ so that you are able to see how $\hat \pi$ approaches $\pi$ by increasing $N$. (for example, use $N$ log-scaled)

2. Save data as array with informations: how many points have been used and estimation of pi.

3. Plot the data.

```python

```


## Spherically symmetric waves

Spherically symmetric three-dimensional
waves propagate in the radial direction $r$ only so that
$u = u(r,t)$. The fully three-dimensional wave equation

$$
\frac{\partial^2u}{\partial t^2}=\nabla\cdot (c^2\nabla u) + f
$$

then reduces to the spherically symmetric wave equation

$$
\begin{equation}
\frac{\partial^2u}{\partial t^2}=\frac{1}{r^2}\frac{\partial}{\partial r}
\left(c^2(r)r^2\frac{\partial u}{\partial t}\right)
+ f(r),\quad r\in (0,R),\ t>0
\thinspace .
\label{_auto1} \tag{1}
\end{equation}
$$

Assume that the wave velocity $c$ is constant. One can easily show
that the function $v(r,t) = ru(r,t)$ fulfills a standard wave equation
in Cartesian coordinates. To this end, insert $u=v/r$ in

$$
\frac{1}{r^2}\frac{\partial}{\partial r}
\left(c^2(r)r^2\frac{\partial u}{\partial t}\right)
$$

to obtain

$$
r\left(\frac{d c^2}{dr}\frac{\partial v}{\partial r} +
c^2\frac{\partial^2 v}{\partial r^2}\right) - \frac{d c^2}{dr}v
\thinspace .
$$

The two terms in the parenthesis can be combined to

$$
r\frac{\partial}{\partial r}\left( c^2\frac{\partial v}{\partial r}\right),
$$

which is recognized as the variable-coefficient Laplace operator in
one Cartesian coordinate. The spherically symmetric wave equation in
terms of $v(r,t)$ now becomes

$$
\begin{equation}
\frac{\partial^2u}{\partial t^2}=
\frac{\partial}{\partial r}
\left(c^2(r)\frac{\partial v}{\partial t}
-\frac{1}{r}\frac{d c^2}{dr}v\right) + rf(r),\quad r\in (0,R),\ t>0
\thinspace .
\label{_auto2} \tag{2}
\end{equation}
$$

In the case of constant wave velocity $c$, this equation reduces to
the wave equation in a single Cartesian coordinate:

$$
\begin{equation}
\frac{\partial^2u}{\partial t^2}=
\frac{\partial}{\partial r}
\left(c^2(r)\frac{\partial v}{\partial t}\right)
+ rf(r),\quad r\in (0,R),\ t>0
\thinspace .
\label{wave:app:rsymm:Cart} \tag{3}
\end{equation}
$$

That is, any program for solving the one-dimensional wave equation
in a Cartesian coordinate system can be used to
solve ([3](#wave:app:rsymm:Cart)), provided the source term is
multiplied by the coordinate. Moreover, if $r=0$ is included in the
domain, spherical symmetry demands that $\partial u/\partial r=0$ at
$r=0$, which means that

$$
\frac{\partial u}{\partial r} = \frac{1}{r^2}\left(
r\frac{\partial v}{\partial r} - v\right) = 0,\quad r=0,
$$

implying $v(0,t)=0$ as a necessary condition. For practical applications,
we exclude $r=0$ from the domain and assume that some boundary
condition is assigned at $r=\epsilon$, for some $\epsilon >0$.