```python
from datascience import *
import sympy
solve = lambda x,y: sympy.solve(x-y)[0] if len(sympy.solve(x-y))==1 else "Not Single Solution"
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import datetime as dt
import warnings
warnings.simplefilter("ignore")
%matplotlib inline
```

# Cournot Competition

One model of understanding oligopolies comes in **Cournot competition**, named for Aontoine Cournot who first proposed it. Cournot competition is a model describing a market in which firms compete by changing their output. In Cournot competition, there are a fixed number of firms in a market that produce the same product. Firms do not collude but have _market power_ (i.e. each firm's output decisions affect the price of the good). Each firm knows the number of firms in the market and has its own cost function that it uses to determine its level of output.

OPEC is a good example of a Cournot oligopoly: its participants affect prices by changing their output. OPEC also demonstrates a flaw in the reasoning behind Cournot competition: the equilibrium state of a Cournot oligopoly suggests that collusion by market participants is the rational policy, but in reality game theory shows us this cannot be the "true" equilibrium because cartel members undercut one another in a bid for market share.

## Cournot Equilibrium

To find the equilibrium state of a Cournot duopoly, let $c$ be the (constant) marginal cost of both firms (i.e. each additional good costs the same amount to produce regardless of the number currently being produced), $p_1$ and $p_2$ be the prices of firms 1 and 2, respectively, and $q_1$ and $q_2$ their quantities. Let $P$ be the price function for a given level of production. We know that the equilibrium price is $p_1 = p_2 = P(q_1 + q_2)$, and therefore firm 1's profit is $q_1(P(q_1 + q_2) - c)$ (since $c$ is constant).

The first step is to calculate firm 1's residual demand. If firm 1 believes that firm 2 is going to produce $q_2$ units of output, we can draw a **residual demand curve** for firm 1. If firm 1 decides to produce 0 units of output, then the price of the good is $P(0 + q_2) = P(q_2)$. If they produce $q_1$ units, it's $P(q_1 + q_2)$. Using this information, we can draw the residual demand curve. This curve will be called $d_1$.

Now we need to determine firm 1's optimal output, which occurs where the **marginal revenue** (the additional revenue gained by increasing production by one unit) intersects the marginal cost $c$. The marginal revenue curve is the line with twice the slope of the residual demand and the same $y$-intercept. We can show this by taking the derivative of the revenue function $\text{TR} = P(Q) \cdot Q$ where $\text{TR}$ is the total revenue. If the demand curve $P(Q)$ has slope $m$ and $y$-intercept $b$, this is

$$\begin{aligned}
\text{TR} &= Q (mQ + b) \\
&= m Q^2 + bQ \\
\frac{\text{d}}{\text{d} Q} \text{TR} &= 2mQ + b
\end{aligned}$$

Thus, the marginal revenue curve is a line with slope $2m$ and $y$-intercept $b$. The marginal revenue of firm 1 is denoted $r_1$ and is a function of $q'_1$.

Let $q'_1$ be the optimum output for firm 1, such that $r_1(q'_1) = c$. The graph below shows the curves and their relationships to one another.

```python
m, b = -0.5, 9
q_2 = 4
c = 3
D_fn = lambda x: m * x + b + 2
P = lambda x: D_fn(x + q_2)
r_1_fn = lambda x: 2 * m * x + m * q_2 + b + 2
r_1_inv = lambda x: (x - m * q_2 - b - 2) / 2 / m

q_1 = np.linspace(0, 25, 1000)
D = D_fn(q_1)
d_1 = P(q_1)
r_1 = r_1_fn(q_1)

q_1_prime = r_1_inv(c)

plt.figure(figsize=[8,8])

# demand
plt.plot(q_1, D, label=r"Market Demand, $D$")
plt.text(5, b, r"$D$", size=16)

# residual demand
plt.plot(q_1, d_1, label=r"Residual Demand of Firm 1, $d_1$")
plt.text(5, b - 2, r"$d_1$", size=16)

# marginal revenue
plt.plot(q_1, r_1, label=r"Marginal Revenue of Firm 1, $r_1$")
plt.text(5, b - 4.5, r"$r_1$", size=16)

# marginal cost
plt.plot(q_1, c * np.ones_like(q_1), label=r"Marginal Cost, $c$")
plt.text(20, 3.2, r"$c$", size=16)

# optimum quantity
plt.vlines(q_1_prime, 0, 3, linestyles="dashed")

# q2
plt.vlines(q_2, 0, D_fn(q_2), linestyles="dashed")
plt.hlines(P(0), 0, q_2, linestyles="dashed")

plt.xlim([0,22])
plt.ylim([0,12])
plt.xticks([r_1_inv(c), q_2], [r"$q^\prime_1$", r"$q_2$"], size=14)
plt.yticks([P(0)], [r"$P(q_2)$"], size=14)
plt.xlabel("Quantity", size=16)
plt.ylabel("Price", size=16)
plt.legend();
```

$q'_1$ is a function of $q_2$ as firm 1's optimal output depends on what it thinks firm 2 is going to produce. To find the equilibrium, we must define $q'_1$ for all possible values of $q_2$. Consider the case when $q_2=0$; then firm 1's residual demand is the same as the market demand, since $d_1$ and $D$ are separated horizontally by a difference of $q_2$. Firm 1 then produces the monopoly quantity $q_m$, since that is where its marginal revenue equals its marginal cost. If, however, firm 2 chooses to produce at a level of perfect competition, $q_c$, the optimum for firm 1 is to produce 0 output units, since their marginal revenue will intersect $c$ at $q_1 = 0$. The graph below shows both of these cases.

```python
m, b = -0.5, 9
q_2 = 0
c = 3
D_fn = lambda x: m * x + b + 2
P = lambda x: D_fn(x + q_2)
r_1_fn = lambda x: 2 * m * x + m * q_2 + b + 2
r_1_inv = lambda x: (x - m * q_2 - b - 2) / 2 / m

q_1 = np.linspace(0, 25, 1000)
D = D_fn(q_1)
d_1 = P(q_1)
r_1 = r_1_fn(q_1)

q_1_prime = r_1_inv(c)

plt.figure(figsize=[8,8])

# demand
plt.plot(q_1, D, label=r"Market Demand, $D$", linewidth=4)
plt.text(5, b, r"$D = d_1$", size=16)

# residual demand
plt.plot(q_1, d_1, label=r"Residual Demand of Firm 1 for $q_2=0$, $d_1$", linestyle="dashed", linewidth=3)
# plt.text(5, b - 2, r"$d_1$", size=16)

# marginal revenue
plt.plot(q_1, r_1, label=r"Marginal Revenue of Firm 1 for $q_2=0$, $r_1$")
plt.text(5, b - 2.5, r"$r_1$", size=16)

# case when q_2 = q_c
m, b = -0.5, 9
q_2 = 16
c = 3
D_fn = lambda x: m * x + b + 2
P = lambda x: D_fn(x + q_2)
r_1_fn = lambda x: 2 * m * x + m * q_2 + b + 2
r_1_inv = lambda x: (x - m * q_2 - b - 2) / 2 / m

q_1 = np.linspace(0, 25, 1000)
D = D_fn(q_1)
d_1 = P(q_1)
r_1 = r_1_fn(q_1)

# residual demand
plt.plot(q_1, d_1, label=r"Residual Demand of Firm 1 for $q_2=q_c$, $d_1$", linestyle="dashed", linewidth=3)
plt.text(2.5, 2, r"$d_1$", size=16)

# marginal revenue
plt.plot(q_1, r_1, label=r"Marginal Revenue of Firm 1 for $q_2=q_c$, $r_1$")
plt.text(2.5, 0.8, r"$r_1$", size=16)

# marginal cost
plt.plot(q_1, c * np.ones_like(q_1), label=r"Marginal Cost, $c$")
plt.text(20, 3.2, r"$c$", size=16)

# q_c
plt.vlines(D_fn(-10), 0, c, linestyles="dashed")

# q'_1(0)
plt.vlines(r_1_inv(-5), 0, 3, linestyles="dashed")

plt.xlim([0,22])
plt.ylim([0,12])
plt.xticks([D_fn(-10), r_1_inv(-5), 0], [r"$q_c$", r"$q'_1(0) = q_m$", r"$q'_1(q_c)$"], size=14)
plt.yticks([], [], size=14)
plt.xlabel("Quantity", size=16)
plt.ylabel("Price", size=16)
plt.legend();
```

When demand is linear and marginal cost is constant, the function $q'_1$ is also linear. Because we have two points, $q'_1(0) = q_m$ and $q'_1(q_c) = 0$, we can draw the curve $q'_1$. Remember that $q'_1$ is a function of what firm 1 *thinks* firm 2 is going to produce.

```python
q_m = r_1_inv(-5)
q_c = D_fn(-10)

p1 = (0, q_m)
p2 = (q_c, 0)

slope = (p2[1] - p1[1]) / (p2[0]- p1[0])
intercept = p1[1] - slope * p1[0]

xs = np.linspace(0, 25, 1000)
ys = slope * xs + intercept

plt.figure(figsize=[8,8])

plt.plot(xs, ys, label=r"$q'_1$")
plt.text(5, intercept - 1.8, r"$q'_1(q_2)$", size=16)

plt.xlim([0,22])
plt.ylim([0,12])
plt.xticks([q_c], [r"$q_c$"], size=14)
plt.yticks([q_m], [r"$q_m$"], size=14)
plt.xlabel("$q_2$", size=16)
plt.ylabel("$q'_1$", size=16);
```

Finally, we need to know firm 2's reaction to firm 1's production. Because both firms have the same cost function, firm 2's optimum output $q'_2$ given firm 1's output $q_1$ is the inverse of firm 1's. The equilibrium of the model is at the intersection of these reaction functions.

```python
q_m = r_1_inv(-5)
q_c = D_fn(-10)

p1 = (0, q_m)
p2 = (q_c, 0)

slope = (p2[1] - p1[1]) / (p2[0]- p1[0])
intercept = p1[1] - slope * p1[0]

xs = np.linspace(0, 25, 1000)
ys1 = slope * xs + intercept
ys2 = (xs - intercept) / slope

plt.figure(figsize=[8,8])

plt.plot(xs, ys1, label=r"$q'_1$")
plt.plot(xs, ys2, label=r"$q'_2$")
plt.text(13, intercept - 6.3, r"$q'_1(q_2)$", size=16)
plt.text(3, intercept + 2.5, r"$q'_2(q_1)$", size=16)

x_star = - (slope + 1) * intercept / (slope**2 - 1)
y_star = slope * x_star + intercept
plt.vlines(x_star, 0, y_star, linestyle="dashed")
plt.hlines(y_star, 0, x_star, linestyle="dashed")

plt.xlim([0,22])
plt.ylim([0,12])
plt.xticks([x_star], [r"$q_2$"], size=14)
plt.yticks([y_star], [r"$q_1$"], size=14);
```

## Implications

The Cournot model implies that output is greater in a Cournot duopoly than in a monopoly, but still lower than perfect competition. Prices are also lower in a Cournot duopoly, but higher than perfect competition. Cournot equilibria are also a subset of Nash equilibria, and so the equilibrium we just derived is one from which neither player will likely deviate. As noted earlier, Cournot also indicates that members of a duopoly could form a cartel and raise profits by colluding.

## Applying Cournot

Now that we have shown how to derive the Cournot equilibrium, let's apply this to a problem. Consider the industry of airline manufacturing: there are two main competitors, Boeing and Airbus, and for this problem we will think of this market as a Cournot duopoly. Suppose the market demand curve is given by $P = -1.89 Q + 148.89$ where the price is in millions of \$ and that the marginal cost of both firms is constant at $c = 100$.

```python
def P_fn(Q):
    return -1.89 * Q + 148.89

Qs = np.linspace(-100, 100, 1000)
Ps = P_fn(Qs)

plt.figure(figsize=[7,7])
plt.plot(Qs, Ps, label=r"$P(Q)$")
plt.hlines(100, -1, 100, color="r", label=r"$c$")
plt.xlim(0, 81)
plt.ylim(0, 150)
plt.xlabel(r"Quantity $Q$", size=16)
plt.ylabel(r"Price $P$", size=16)
plt.legend();
```

Now suppose we want to find the equilibrium and that Boeing believes that Airbus will produce at quantity $q_2 = 20$. To find the equilibrium, we need to start by finding Boeing's residual demand curve. This is given by

$$\begin{aligned}
d_1(q_1) &= P(q_1 + q_2) \\
&= -1.89(q_1 + q_2) + 148.89 \\
&= -1.89(q_1 + 20) + 148.89 \\
&= -1.89 q_1 + 111.09
\end{aligned}$$

Now we need the marginal revenue $r_1$ of Boeing, which is the line with the same $y$-intercept but twice the slope. We can easily find $r_1$ as $2 d_1(q_1) - d_1(0)$ as this will double the slope for us but subtract out half of the doubled $y$-intercept.

$$\begin{aligned}
r_1(q_1) &= 2 \cdot d_1(q_1) - d_1(0) \\
&= -3.78 + 111.09
\end{aligned}$$

```python
def P_fn(Q):
    return -1.89 * Q + 148.89

q_2 =  20

Qs = np.linspace(-100, 100, 1000)
Ps = P_fn(Qs)
d_1 = P_fn(Qs + q_2)
r_1 =  2 * P_fn(Qs + q_2) - P_fn(q_2)

plt.figure(figsize=[7,7])
plt.plot(Qs, Ps, label=r"$P(Q)$")
plt.plot(Qs, d_1, label=r"$d_1(Q)$")
plt.plot(Qs, r_1, label=r"$r_1(Q)$")
plt.hlines(100, -1, 100, color="r", label=r"$c$")
plt.xlim(0, 35)
plt.ylim(95, 150)
plt.xlabel(r"Quantity $Q$", size=16)
plt.ylabel(r"Price $P$", size=16)
plt.legend();
```

Based on this graph, if Boeing's prediction that Airbus will produce 20 units of output is correct, we can already tell what Boeing's optimal output is; but instead, let's use SymPy to calculate it.

```python
c = 100
q_1 = sympy.Symbol("q_1")
r_1 = -3.78 * q_1 + 111.09

solve(r_1, c)
```

    2.93386243386243

Therefore, if Boeing thinks that Airbus will produce 20 planes, they should produce 3.

Now, however, let's say that Boeing is unsure of it's guess for $q_2$. How can we arrive at the _actual_ Cournot equilibrium, the quantities at which both Boeing and Airbus should produce? We need to start by finding the monopoly quantity $q_m$ and the perfect competition quantity $q_c$. We can find $q_m$ by looking at Boeing's marginal revenue when Airbus is assumed to produce 0 planes. In this case, $d_1 = P(q_1 + 0) = -1.89 q_1 + 148.89$ and therefore $r_1 = -3.78 q_1 + 148.89$. We find $q_m$ as the point at which this new marginal revenue curve intersects the marginal cost curve.

```python
r_1 = -3.78 * q_1 + 148.89
q_m = solve(r_1, c)
q_m
```

    12.9338624338624

So the monopoly quantity is $q_m = 12.934$. To find the perfect competition quantity $q_c$, we need to find the point at which the market demand curve $P$ intersects the marginal cost.

```python
Q = sympy.Symbol("Q")
P = -1.89 * Q + 148.89
q_c =  solve(P, c)
q_c
```

    25.8677248677249

And thus we get $q_c = 25.868$. Now, we know that when Airbus is producing at $q_c$, Boeing is producing 0 planes because their marginal revenue is below the marginal cost for every value above 0. When Airbus is producing 0 planes, Boeing produces at $p_m$. From this, we have two points with which we can draw Boeing's reaction curve to Airbus's quantity choice $q'_1(q_2)$:

```python
x1, y1 = 0, q_m
x2, y2 = q_c, 0

m = (y2 - y1) / (x2 - x1)
b = y1 - m * x1

print(f"y = {m} * x + {b}")
```

    y = -0.500000000000000 * x + 12.9338624338624

Because the marginal cost is constant and the same for both Boeing and Airbus, Airbus's reaction function $q'_2(q_1)$ is symmetrical to Boeing's:

```python
xs = np.linspace(-10, 30, 100)
ys1 = m * xs + b
ys2 = (xs - b) / m

plt.figure(figsize=[7,7])
plt.plot(xs, ys1, label=r"$q'_1(q_2)$")
plt.plot(xs, ys2, label=r"$q'_2(q_1)$")
plt.xlim([0, 27])
plt.ylim([0, 27])
plt.xlabel(r"$q_2$", size=16)
plt.ylabel(r"$q_1$", size=16)
plt.legend();
```

To get the exact values, let's use SymPy one last time:

```python
q_2 = sympy.Symbol("q_2")
q_1_prime = -.5 * q_2 + q_m
q_2_prime = (q_2 - q_m) / (-.5)

q_2_star = solve(q_1_prime, q_2_prime)
q_1_star = q_1_prime.subs(q_2, q_2_star)
print(f"q_1_star = {q_1_star}\nq_2_star = {q_2_star}")
```

    q_1_star = 8.62257495590830
    q_2_star = 8.62257495590827


# <ins>Lab #2: Basics + Loops, Conditionals & Functions </ins>

These exercises are meant to give you some practice with the concepts from Tutorials 2.1-2.3, as well as Tutorials 1.1/1.2 since we didn't get to the exercises last week. Be sure to use best practices, comment your code, and be mindful of your units.

```python
# Import any libraries here:

```

***
## Arithmetic & variables

1. Use the quadratic equation, to find the roots of $5x^2 + 3x - 1$ by calculating the numerator and denominator separately. Round to 2 decimal places. (**Tip:** When parintheses in a big equation get a little hard to read, breaking it up like this is a good way to make code easier to read.)

$$x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}$$

```python
# Your code here:

```

2. Use the distance modulus, to find the distance to a star with apparent magnitude $m_v = 0.5$ and absolute magnitude $M_v = -5.85$.

$$m_v - M_v = 5log(d/10 \text{ pc})$$

```python
# Your code here:

```

***
## User input

Prompt the user for their age and calculate the percent of their life they've completed assuming they live to 80 years old.

```python
# Your code here:

```

***
## Indexing

1. Print the strings "pneumono," "ultra," "volcano," and "coniosis" by slicing the word "pneumonoultramicroscopicsilicovolcanoconiosis".

```python
# Your code here:

```

2. Print the 4th, 9th, and 15th letter and the letter at the 20th, 3rd, and 12th indices of the word from part one by indexing.

```python
# Your code here:

```

***
## `numpy` arrays

Use the Wein displacement law to calculate the temperatures of blackbodies with 20 peak wavelengths between 300nm and 700nm using arrays.

\begin{equation}
    \lambda_{peak}T = 0.29 \text{ cm K}
\end{equation}

```python
# Your code here:

```

***
## Conditional statements

Write a program that has the user input a wavelength or frequency and returns (a) the photon energy, and (b) the part of the electromagnetic spectrum in which a photon of that energy falls.

$$E = h\nu \quad\text{&}\quad \lambda\nu = c$$

```python
# Your code here:

```

***
## Loops and plotting

1. Add together every third number in the list [155,2,54,34,5,16,7,38,26,10] and print the result.

```python
# Your code here:

```

2. Plot the sine and cosine functions on [-2$\pi$,2$\pi$].

```python
# Your code here:

```

3. The rules for Pin-Pon are as follows:

    - Start counting numbers, starting from 1.
    - Whenever the next number's a multiple of 3 (3, 6, 9,…), replace the actual number with, "Pin."
    - Whenever the next number's a multiple of 5, replace it with the word, "Pon."
    - Whenever the number's a multiple of both 3 and 5, say, "Pin Pon."
    - Keep going until someone messes up.

Write a program that prints the results of the game for 1 through 100.

```python
# Your code here:

```

***
## Putting it all together

These are meant to use all of your new skills at once. That being said, you may have notice that there's no "Functions" section above --- once you've completed each of the tasks below, please turn them into functions, comment them well

1. (From Lab 1): While DMS (degrees, minutes, seconds) and HMS (hours, minutes, seconds) formats for celestial coordinates have their place (e.g. in every database ever), having them in decimal degrees is often more convenient for calculations. Write two scripts: one that allows the user to enter in a DMS coordinate and prints it in decimal degrees, and another for HMS coordinates.

```python
# Your code here:

```

2. In my research on X-ray binaries, I utilize the Novikov-Thorne thin accretion disk approximation for black holes, which relates the temperature of the accertion disk to the mass of the accretor. Typical disk temperatures range between 0.5 and 1.0 keV. Using this relationship, calculate the mass of black holes (a) within this range, (b) in the "interesting" regime of 1.0 to 3.0 keV, and (c) in the "crazy" range of >5.0 keV. Plot your results, making sure the three regimes are clearly labeled in your plot.

$$T_{eff} \sim \Bigg(\frac{10M_\odot}{M}\Bigg)^{1/4} \text{ keV}$$

```python
# Your code here:

```


```python
from sympy import cos, sin, pi, simplify, symbols, sqrt, atan2
from sympy.matrices import Matrix
import numpy as np
```

```python
q1, q2, q3, q4, q5, q6, q7 = symbols('q1:8')  #Theta
d1, d2, d3, d4, d5, d6, d7 = symbols('d1:8')  #d-offset

a0, a1, a2, a3, a4, a5, a6 = symbols('a0:7')  #
alpha0, alpha1, alpha2, alpha3, alpha4, alpha5, alpha6 = symbols('alpha0:7')  # alpha twist angle..
```

```python
#DH Parameters Dictionary

dh = {
    a0: 0,      alpha0: 0,      q1: q1,        d1: 0.75,
    a1: 0.35,   alpha1: pi/2,   q2: q2,        d2: 0.00,
    a2: 1.25,   alpha2: 0,      q3: q3 + pi/2, d3: 0.00,
    a3: -0.054, alpha3: pi/2,   q4: q4,        d4: 1.50,
    a4: 0,      alpha4: pi/2,   q5: q5,        d5: 0.00,
    a5: 0,      alpha5: pi/2,   q6: q6,        d6: 0.00,
    a6: 0,      alpha6: 0,      q7: 0,         d7: 0.303
}
```

```python
#Homogeneous Transformations

T0_1 = Matrix([[cos(q1),             -sin(q1),            0,            a0],
               [sin(q1)*cos(alpha0), cos(alpha0)*cos(q1), -sin(alpha0), -sin(alpha0)*d1],
               [sin(alpha0)*sin(q1), sin(alpha0)*cos(q1), cos(alpha0),  cos(alpha0)*d1],
               [0,                   0,                   0,            1]])

T1_2 = Matrix([[cos(q2),             -sin(q2),            0,            a1],
               [sin(q2)*cos(alpha1), cos(alpha1)*cos(q2), -sin(alpha1), -sin(alpha1)*d2],
               [sin(alpha1)*sin(q2), sin(alpha1)*cos(q2), cos(alpha1),  cos(alpha1)*d2],
               [0,                   0,                   0,            1]])

T2_3 = Matrix([[cos(q3),             -sin(q3),            0,            a2],
               [sin(q3)*cos(alpha2), cos(alpha2)*cos(q3), -sin(alpha2), -sin(alpha2)*d3],
               [sin(alpha2)*sin(q3), sin(alpha2)*cos(q3), cos(alpha2),  cos(alpha2)*d3],
               [0,                   0,                   0,            1]])

T3_4 = Matrix([[cos(q4),             -sin(q4),            0,            a3],
               [sin(q4)*cos(alpha3), cos(alpha3)*cos(q4), -sin(alpha3), -sin(alpha3)*d4],
               [sin(alpha3)*sin(q4), sin(alpha3)*cos(q4), cos(alpha3),  cos(alpha3)*d4],
               [0,                   0,                   0,            1]])

T4_5 = Matrix([[cos(q5),             -sin(q5),            0,            a4],
               [sin(q5)*cos(alpha4), cos(alpha4)*cos(q5), -sin(alpha4), -sin(alpha4)*d5],
               [sin(alpha4)*sin(q5), sin(alpha4)*cos(q5), cos(alpha4),  cos(alpha4)*d5],
               [0,                   0,                   0,            1]])

T5_6 = Matrix([[cos(q6),             -sin(q6),            0,            a5],
               [sin(q6)*cos(alpha5), cos(alpha5)*cos(q6), -sin(alpha5), -sin(alpha5)*d6],
               [sin(alpha5)*sin(q6), sin(alpha5)*cos(q6), cos(alpha5),  cos(alpha5)*d6],
               [0,                   0,                   0,            1]])

T6_G = Matrix([[cos(q7),             -sin(q7),            0,            a6],
               [sin(q7)*cos(alpha6), cos(alpha6)*cos(q7), -sin(alpha6), -sin(alpha6)*d7],
               [sin(alpha6)*sin(q7), sin(alpha6)*cos(q7), cos(alpha6),  cos(alpha6)*d7],
               [0,                   0,                   0,            1]])

```

```python
T0_1 = T0_1.subs(dh)
T1_2 = T1_2.subs(dh)
T2_3 = T2_3.subs(dh)
T3_4 = T3_4.subs(dh)
T4_5 = T4_5.subs(dh)
T5_6 = T5_6.subs(dh)
T6_G = T6_G.subs(dh)
```

```python
print(T6_G)
```

    Matrix([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0.303000000000000], [0, 0, 0, 1]])

```python
T0_G = simplify(T0_1 * T1_2 * T2_3 * T3_4 * T4_5 * T5_6 * T6_G)
```

```python
print(T0_G)
```

    Matrix([[((sin(q1)*sin(q4) - sin(q2 + q3)*cos(q1)*cos(q4))*cos(q5) + sin(q5)*cos(q1)*cos(q2 + q3))*cos(q6) - (sin(q1)*cos(q4) + sin(q4)*sin(q2 + q3)*cos(q1))*sin(q6), ((-sin(q1)*sin(q4) + sin(q2 + q3)*cos(q1)*cos(q4))*cos(q5) - sin(q5)*cos(q1)*cos(q2 + q3))*sin(q6) - (sin(q1)*cos(q4) + sin(q4)*sin(q2 + q3)*cos(q1))*cos(q6), (sin(q1)*sin(q4) - sin(q2 + q3)*cos(q1)*cos(q4))*sin(q5) - cos(q1)*cos(q5)*cos(q2 + q3), 0.303*sin(q1)*sin(q4)*sin(q5) - 0.303*sin(q5)*sin(q2 + q3)*cos(q1)*cos(q4) + 0.054*sin(q2 + q3)*cos(q1) + 1.25*cos(q1)*cos(q2) - 0.303*cos(q1)*cos(q5)*cos(q2 + q3) + 1.5*cos(q1)*cos(q2 + q3) + 0.35*cos(q1)], [-((sin(q1)*sin(q2 + q3)*cos(q4) + sin(q4)*cos(q1))*cos(q5) - sin(q1)*sin(q5)*cos(q2 + q3))*cos(q6) - (sin(q1)*sin(q4)*sin(q2 + q3) - cos(q1)*cos(q4))*sin(q6), ((sin(q1)*sin(q2 + q3)*cos(q4) + sin(q4)*cos(q1))*cos(q5) - sin(q1)*sin(q5)*cos(q2 + q3))*sin(q6) - (sin(q1)*sin(q4)*sin(q2 + q3) - cos(q1)*cos(q4))*cos(q6), -(sin(q1)*sin(q2 + q3)*cos(q4) + sin(q4)*cos(q1))*sin(q5) - sin(q1)*cos(q5)*cos(q2 + q3), -0.303*sin(q1)*sin(q5)*sin(q2 + q3)*cos(q4) + 0.054*sin(q1)*sin(q2 + q3) + 1.25*sin(q1)*cos(q2) - 0.303*sin(q1)*cos(q5)*cos(q2 + q3) + 1.5*sin(q1)*cos(q2 + q3) + 0.35*sin(q1) - 0.303*sin(q4)*sin(q5)*cos(q1)], [(sin(q5)*sin(q2 + q3) + cos(q4)*cos(q5)*cos(q2 + q3))*cos(q6) + sin(q4)*sin(q6)*cos(q2 + q3), -(sin(q5)*sin(q2 + q3) + cos(q4)*cos(q5)*cos(q2 + q3))*sin(q6) + sin(q4)*cos(q6)*cos(q2 + q3), sin(q5)*cos(q4)*cos(q2 + q3) - sin(q2 + q3)*cos(q5), 1.25*sin(q2) + 0.303*sin(q5)*cos(q4)*cos(q2 + q3) - 0.303*sin(q2 + q3)*cos(q5) + 1.5*sin(q2 + q3) - 0.054*cos(q2 + q3) + 0.75], [0, 0, 0, 1]])

```python
T0_2 = simplify(T0_1 * T1_2)
T0_3 = simplify(T0_2 * T2_3)
T0_4 = simplify(T0_3 * T3_4)
T0_5 = simplify(T0_4 * T4_5)
T0_6 = simplify(T0_5 * T5_6)
T0_G = simplify(T0_6 * T6_G)

print(T0_G)
```

    Matrix([[((sin(q1)*sin(q4) - sin(q2 + q3)*cos(q1)*cos(q4))*cos(q5) + sin(q5)*cos(q1)*cos(q2 + q3))*cos(q6) - (sin(q1)*cos(q4) + sin(q4)*sin(q2 + q3)*cos(q1))*sin(q6), -((sin(q1)*sin(q4) - sin(q2 + q3)*cos(q1)*cos(q4))*cos(q5) + sin(q5)*cos(q1)*cos(q2 + q3))*sin(q6) - (sin(q1)*cos(q4) + sin(q4)*sin(q2 + q3)*cos(q1))*cos(q6), (sin(q1)*sin(q4) - sin(q2 + q3)*cos(q1)*cos(q4))*sin(q5) - cos(q1)*cos(q5)*cos(q2 + q3), 0.303*(sin(q1)*sin(q4) - sin(q2 + q3)*cos(q1)*cos(q4))*sin(q5) + (0.054*sin(q2 + q3) + 1.25*cos(q2) + 1.5*cos(q2 + q3) + 0.35)*cos(q1) - 0.303*cos(q1)*cos(q5)*cos(q2 + q3)], [-((sin(q1)*sin(q2 + q3)*cos(q4) + sin(q4)*cos(q1))*cos(q5) - sin(q1)*sin(q5)*cos(q2 + q3))*cos(q6) - (sin(q1)*sin(q4)*sin(q2 + q3) - cos(q1)*cos(q4))*sin(q6), ((sin(q1)*sin(q2 + q3)*cos(q4) + sin(q4)*cos(q1))*cos(q5) - sin(q1)*sin(q5)*cos(q2 + q3))*sin(q6) - (sin(q1)*sin(q4)*sin(q2 + q3) - cos(q1)*cos(q4))*cos(q6), -(sin(q1)*sin(q2 + q3)*cos(q4) + sin(q4)*cos(q1))*sin(q5) - sin(q1)*cos(q5)*cos(q2 + q3), -0.303*(sin(q1)*sin(q2 + q3)*cos(q4) + sin(q4)*cos(q1))*sin(q5) + (0.054*sin(q2 + q3) + 1.25*cos(q2) + 1.5*cos(q2 + q3) + 0.35)*sin(q1) - 0.303*sin(q1)*cos(q5)*cos(q2 + q3)], [(sin(q5)*sin(q2 + q3) + cos(q4)*cos(q5)*cos(q2 + q3))*cos(q6) + sin(q4)*sin(q6)*cos(q2 + q3), -(sin(q5)*sin(q2 + q3) + cos(q4)*cos(q5)*cos(q2 + q3))*sin(q6) + sin(q4)*cos(q6)*cos(q2 + q3), sin(q5)*cos(q4)*cos(q2 + q3) - sin(q2 + q3)*cos(q5), 1.25*sin(q2) + 0.303*sin(q5)*cos(q4)*cos(q2 + q3) - 0.303*sin(q2 + q3)*cos(q5) + 1.5*sin(q2 + q3) - 0.054*cos(q2 + q3) + 0.75], [0, 0, 0, 1]])

```python
# Correction for gripper link final axis orientation.
R_y = Matrix([[cos(-pi/2),  0, sin(-pi/2), 0],
	      [0,           1, 0,           0],
	      [-sin(-pi/2), 0, cos(-pi/2),  0],
	      [0, 0, 0, 1]])
R_z = Matrix([[cos(pi), -sin(pi), 0,  0],
	      [sin(pi), cos(pi),  0,  0],
	      [0, 0, 1, 0],
	      [0, 0, 0, 1]])

R_cor = simplify(R_z * R_y)
T_total = simplify(T0_G * R_cor)
```

```python
print(R_y)
```

    Matrix([[0, 0, -1, 0], [0, 1, 0, 0], [1, 0, 0, 0], [0, 0, 0, 1]])

```python
print("Total = ", T_total.evalf(subs={q1:-0.97, q2:-0.39, q3:0.72, q4:1.10, q5:-1.10, q6:0}))
```

    Total =  Matrix([[0.486634700919385, -0.210912373212720, -0.847763315251892, 1.81095620663497], [0.694910440520268, -0.494636033789280, 0.521952750479522, -2.21683181038251], [-0.529420577156790, -0.843119899482267, -0.0941418481865706, 0.549328302259311], [0, 0, 0, 1.00000000000000]])

```python
print(T_total.shape)
```

    (4, 4)

```python
c = T_total[0:3, 0:3]
print(c)
print(c.T)
```

    Matrix([[(sin(q1)*sin(q4) - sin(q2 + q3)*cos(q1)*cos(q4))*sin(q5) - cos(q1)*cos(q5)*cos(q2 + q3), ((sin(q1)*sin(q4) - sin(q2 + q3)*cos(q1)*cos(q4))*cos(q5) + sin(q5)*cos(q1)*cos(q2 + q3))*sin(q6) + (sin(q1)*cos(q4) + sin(q4)*sin(q2 + q3)*cos(q1))*cos(q6), ((sin(q1)*sin(q4) - sin(q2 + q3)*cos(q1)*cos(q4))*cos(q5) + sin(q5)*cos(q1)*cos(q2 + q3))*cos(q6) - (sin(q1)*cos(q4) + sin(q4)*sin(q2 + q3)*cos(q1))*sin(q6)], [-(sin(q1)*sin(q2 + q3)*cos(q4) + sin(q4)*cos(q1))*sin(q5) - sin(q1)*cos(q5)*cos(q2 + q3), -((sin(q1)*sin(q2 + q3)*cos(q4) + sin(q4)*cos(q1))*cos(q5) - sin(q1)*sin(q5)*cos(q2 + q3))*sin(q6) + (sin(q1)*sin(q4)*sin(q2 + q3) - cos(q1)*cos(q4))*cos(q6), -((sin(q1)*sin(q2 + q3)*cos(q4) + sin(q4)*cos(q1))*cos(q5) - sin(q1)*sin(q5)*cos(q2 + q3))*cos(q6) - (sin(q1)*sin(q4)*sin(q2 + q3) - cos(q1)*cos(q4))*sin(q6)], [sin(q5)*cos(q4)*cos(q2 + q3) - sin(q2 + q3)*cos(q5), (sin(q5)*sin(q2 + q3) + cos(q4)*cos(q5)*cos(q2 + q3))*sin(q6) - sin(q4)*cos(q6)*cos(q2 + q3), (sin(q5)*sin(q2 + q3) + cos(q4)*cos(q5)*cos(q2 + q3))*cos(q6) + sin(q4)*sin(q6)*cos(q2 + q3)]])
    Matrix([[(sin(q1)*sin(q4) - sin(q2 + q3)*cos(q1)*cos(q4))*sin(q5) - cos(q1)*cos(q5)*cos(q2 + q3), -(sin(q1)*sin(q2 + q3)*cos(q4) + sin(q4)*cos(q1))*sin(q5) - sin(q1)*cos(q5)*cos(q2 + q3), sin(q5)*cos(q4)*cos(q2 + q3) - sin(q2 + q3)*cos(q5)], [((sin(q1)*sin(q4) - sin(q2 + q3)*cos(q1)*cos(q4))*cos(q5) + sin(q5)*cos(q1)*cos(q2 + q3))*sin(q6) + (sin(q1)*cos(q4) + sin(q4)*sin(q2 + q3)*cos(q1))*cos(q6), -((sin(q1)*sin(q2 + q3)*cos(q4) + sin(q4)*cos(q1))*cos(q5) - sin(q1)*sin(q5)*cos(q2 + q3))*sin(q6) + (sin(q1)*sin(q4)*sin(q2 + q3) - cos(q1)*cos(q4))*cos(q6), (sin(q5)*sin(q2 + q3) + cos(q4)*cos(q5)*cos(q2 + q3))*sin(q6) - sin(q4)*cos(q6)*cos(q2 + q3)], [((sin(q1)*sin(q4) - sin(q2 + q3)*cos(q1)*cos(q4))*cos(q5) + sin(q5)*cos(q1)*cos(q2 + q3))*cos(q6) - (sin(q1)*cos(q4) + sin(q4)*sin(q2 + q3)*cos(q1))*sin(q6), -((sin(q1)*sin(q2 + q3)*cos(q4) + sin(q4)*cos(q1))*cos(q5) - sin(q1)*sin(q5)*cos(q2 + q3))*cos(q6) - (sin(q1)*sin(q4)*sin(q2 + q3) - cos(q1)*cos(q4))*sin(q6), (sin(q5)*sin(q2 + q3) + cos(q4)*cos(q5)*cos(q2 + q3))*cos(q6) + sin(q4)*sin(q6)*cos(q2 + q3)]])

```python

```


# Aufgabe 3

```python
from sympy import *
init_printing()

from matplotlib import pyplot as plt, ticker as tk
%matplotlib inline
%config InlineBackend.figure_format='retina'

import numpy as np
import pandas as pd

from fun_expr import Function_from_Expression as FE
```

```python
V_ra = 7*8*3.5 # m**3

n = 20 # Personen
dV_sch = n*18e-3 # in m**3/h
dV_sch
```

```python
k_au = 400e-6 # 400 ppM
k_0 = 2400e-6 # 2400 ppM
k_zul = k_inf = 1000e-6 # 1000 ppM

dV_au = dV_sch/(k_zul-k_au)
dV_au
```

```python
beta = dV_au/V_ra
beta
```

```python
t = Symbol('t')

k = FE(t, k_au + (k_0 - k_au)*exp(-beta*t),name='k')
k.equation()
```

$\displaystyle k\left(t\right) = 0.0004 + 0.002 e^{- 3.06122448979592 t}$

```python
fig, ax = plt.subplots()

lt = np.linspace(0,2)
ax.plot(lt,1e6*k.lambdified(lt))

ax.set(
    xlim=(0,2),xlabel='Zeit $t$ in Stunden',
    ylim=(0,2500), ylabel='CO_2-Konzentration $k$ in ppM'
)

ax.axhline(1000,c='r')

ax.grid(lw=0.25, c='k')
```

```python
t_1 = -1/beta*ln((k_au-k_zul)/(k_au-k_0))
t_1, 1e6*k(t_1)
```

```python
fig, ax = plt.subplots()

lt = np.linspace(0,float(t_1))
ax.plot(lt,1e6*k.lambdified(lt),c='b')

lt = np.linspace(float(t_1),2)
ax.plot(lt,1e6*k.lambdified(lt),ls='dotted',c='b')

ax.axhline(1000,c='r')

ax.set(
    xlim=(0,2),xlabel='Zeit $t$ in Stunden',
    ylim=(0,2500), ylabel='CO_2-Konzentration $k$ in ppM'
)

ax.grid(lw=0.25, c='k')
```

```python
t_1*60 # t_1 in Minuten
```

Nach 24 Minuten ist die erlaubte Schadstoffkonzentration von 1000 ppM erreicht.

# Zusatzfrage:

Was passiert, wenn eine Pause von 15 Minuten eingehalten wird?

```python
t_2 = S(1)/4
k_1 = FE(t, Piecewise((k(t),t <= t_2),(k_inf + (k(t_2)-k_inf)*exp(-beta*(t-t_2)),True)))
```

```python
fig, ax = plt.subplots()

lt = np.linspace(0,float(t_2))
ax.plot(lt,1e6*k_1.lambdified(lt),c='b')

lt = np.linspace(float(t_2),2)
ax.plot(lt,1e6*k_1.lambdified(lt),c='b')

ax.axhline(1000,c='r')

ax.set(
    xlim=(0,2),xlabel='Zeit $t$ in Stunden',
    ylim=(0,2500), ylabel='CO_2-Konzentration $k$ in ppM'
)

ax.grid(lw=0.25, c='k')
```


 Computation of the modified equation of a numerical scheme (univariate PDE evolution equation)

    Olivier Pannekoucke
 2020

# Introduction

In this illustration we compute the modified equation assowiated with the Euler discretization and the centered discretization of the advection equation
$$(1)\quad \partial_t u + c \partial_x u =0$$
whose numerical scheme is
$$(2)\quad u^{q+1}_k =  u_k^q - c \delta t \frac{u^q_{k+1}-u^q_{k-1}}{2\delta x}$$
where $u^q_k$ stands for a numerical approximation of $u(q\delta t,k\delta x)$.

## Application to the Euler time scheme applied on the advection

#### Import of modules & functions

```python
from modequation import ModifiedEquation
import sympy
from sympy import symbols, Derivative, Function, Eq
```

```python
c = sympy.symbols('c')
coordinates = sympy.symbols('t x')
t, x = coordinates
dt, dx = sympy.symbols('dt dx')

u = Function('u')(*coordinates)
```

```python
u_qp=u.subs(t,t+dt)
u_kp=u.subs(x,x+dx)
u_km=u.subs(x,x-dx)
```

#### Definition of the time schemes

First we define the numerical schemes as an equation (`sympy.Eq`)
$$(3)\quad \frac{u^{q+1}_k-u_k^q }{\delta t} =  - c \frac{u^q_{k+1}-u^q_{k-1}}{2\delta x}$$

```python
euler_centered_scheme = Eq( (u_qp-u)/dt, -c * (u_kp-u_km)/(2*dx))
```

#### Computation of the modified equations

```python
euler_centered = ModifiedEquation(euler_centered_scheme,u, order =3)
```

```python
euler_centered.consistant_equation
```

$\displaystyle \frac{\partial}{\partial t} u{\left(t,x \right)} = - c \frac{\partial}{\partial x} u{\left(t,x \right)}$

```python
euler_centered.modified_equation
```

$\displaystyle \frac{\partial}{\partial t} u{\left(t,x \right)} = - c \frac{\partial}{\partial x} u{\left(t,x \right)} - \frac{c dx^{2} \frac{\partial^{3}}{\partial x^{3}} u{\left(t,x \right)}}{6} - \frac{c^{2} dt \frac{\partial^{2}}{\partial x^{2}} u{\left(t,x \right)}}{2} + O\left(dx^{3}\right) + O\left(dt^{3}\right)$


```python
from sympy import init_printing; init_printing();
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
```

```python
from silkpy.symbolic.geometry_map.coord_transform import CoordTransform
from silkpy.symbolic.geometry_map.coord_transform.predefined import Cylindrical2Cartesian, Cartesian2Cylindrical

Cylindrical2Cartesian().exprs
Cartesian2Cylindrical().exprs
Cylindrical2Cartesian().jacobian
Cartesian2Cylindrical().jacobian
```

    /home/wenyin/pyvenv/vsym/lib/python3.8/site-packages/IPython/lib/latextools.py:126: MatplotlibDeprecationWarning:
    The to_png function was deprecated in Matplotlib 3.4 and will be removed two minor releases later. Use mathtext.math_to_image instead.
      mt.to_png(f, s, fontsize=12, dpi=dpi, color=color)
    /home/wenyin/pyvenv/vsym/lib/python3.8/site-packages/IPython/lib/latextools.py:126: MatplotlibDeprecationWarning:
    The to_rgba function was deprecated in Matplotlib 3.4 and will be removed two minor releases later. Use mathtext.math_to_image instead.
      mt.to_png(f, s, fontsize=12, dpi=dpi, color=color)
    /home/wenyin/pyvenv/vsym/lib/python3.8/site-packages/IPython/lib/latextools.py:126: MatplotlibDeprecationWarning:
    The to_mask function was deprecated in Matplotlib 3.4 and will be removed two minor releases later. Use mathtext.math_to_image instead.
      mt.to_png(f, s, fontsize=12, dpi=dpi, color=color)
    /home/wenyin/pyvenv/vsym/lib/python3.8/site-packages/IPython/lib/latextools.py:126: MatplotlibDeprecationWarning:
    The MathtextBackendBitmap class was deprecated in Matplotlib 3.4 and will be removed two minor releases later. Use mathtext.math_to_image instead.
      mt.to_png(f, s, fontsize=12, dpi=dpi, color=color)

$\displaystyle \left[\begin{matrix}R \cos{\left(\phi \right)} & R \sin{\left(\phi \right)} & Z\end{matrix}\right]$

$\displaystyle \left[\begin{matrix}\sqrt{x^{2} + y^{2}} & z & \operatorname{atan_{2}}{\left(y,x \right)}\end{matrix}\right]$

$\displaystyle \left[\begin{matrix}\cos{\left(\phi \right)} & \sin{\left(\phi \right)} & 0\\0 & 0 & 1\\- R \sin{\left(\phi \right)} & R \cos{\left(\phi \right)} & 0\end{matrix}\right]$

$\displaystyle \left[\begin{matrix}\frac{x}{\sqrt{x^{2} + y^{2}}} & 0 & - \frac{y}{x^{2} + y^{2}}\\\frac{y}{\sqrt{x^{2} + y^{2}}} & 0 & \frac{x}{x^{2} + y^{2}}\\0 & 1 & 0\end{matrix}\right]$

```python
Cartesian2Cartesian = Cartesian2Cylindrical() | Cylindrical2Cartesian()
Cylindrical2Cylindrical = Cylindrical2Cartesian() | Cartesian2Cylindrical()
```

```python
Cartesian2Cartesian.exprs
Cartesian2Cartesian.syms
```

```python
Cylindrical2Cylindrical.exprs.simplify()
Cylindrical2Cylindrical.syms
```

```python

```


# Second Order ODEs in Python

Our last assignment consisted of a series of first order ODEs. This assignment will be similarly structured; however, you will be using the same techniques to solve second order ODEs. In particular, the examples I will give will be for a damped spring-mass system and coupled spring mass system then the problems you will solve will be for a single damped pendulum and a coupled pendulum.

## ODEs in Python

In this class, we will make use for the [scipy.integrate toolbox](https://docs.scipy.org/doc/scipy/reference/integrate.html). Primarily, we will be accessing the [solve_ivp](https://docs.scipy.org/doc/scipy/reference/generated/scipy.integrate.solve_ivp.html#scipy.integrate.solve_ivp) (for *initial* value problems) and the [solve_bvp](https://docs.scipy.org/doc/scipy/reference/generated/scipy.integrate.solve_bvp.html#scipy.integrate.solve_bvp) (for *boundary* value problems).
Through these two functions, Scipy seeks the best type of solver for a particular system. We'll import these and other necessary elements below.

```python
# Python Imports

# 3rd Party Numerical Imports
import numpy as np
from scipy.integrate import solve_ivp as ivp

# 3rd Party Plotting Utilities
from matplotlib import pyplot as plt
```

## Examples

### The Damped Spring-Mass System

You all may remember that the equation of motion for a damped spring-mass system is

\begin{equation}
    m \ddot{x} + c \dot{x} + k x = 0
\end{equation}

where $m$ is the mass, $c$ is the damping constant and $k$ is the spring constant. Just like we did in the first ODE assignment, we will have to rewrite this equation in terms of the highest order term.

\begin{equation}
    \ddot{x} = -2\gamma \dot{x} - \omega_0^2 x
\end{equation}

where $2\gamma = c/m$ is the normalized damping coefficient and $\omega_0^2 = k/m$ is the natural frequency of the system. Now, the only problem with this equation is that we still have a term of $\dot{x}$ which we will have to write out as a second equation. This way, the differential equation solver will know how $\ddot{x}$, $\dot{x}$ and $x$ are related. We know that $\dot{x} = v$. Therefore, we will tell the solver this information by using a *system* of equations which we will write as

\begin{align}
    \dot{x}  &= v \\
    \dot{v} &= -2\gamma v - \omega_0^2 x.
\end{align}

Now, as it did on the last assignment, the ODE solver in Python expects as its first argument a function that packages all the necessary information about the problem we are trying to solve. And, the specified function is meant to take the independent variable as the first argument ($t$ in this case), the dependent variables as a tuple in the second argument ($X$ in this case) and any other variables after that. To see this visually, check the example and input below.

```python
def odefunc(indepVar, depVars, *otherArgs)
```

```python
def dshm(t, X, gamma, omega0Sq):
    '''A function for the ODE solver that specifies the conditions for damped, simple harmonic oscillations.'''

    # Unpack the variables
    pos, vel = X

    # Return the Output of the Equations
    return [
        vel,
        -2*gamma*vel - omega0Sq*pos
    ]
```

Note from the equations and functions above that, as we have it written the first output will be the *integration* of $\dot{x}$ with respect to time which is the position and the second output will be the integration of $\dot{v} = \ddot{x}$ with respect to time which is the velocity. Essentially, ODE solvers do not know how to handle anything other than a first order ODE. Therefore, we have to reframe our problems of higher orders into ones of a system of first order ODEs.

At this point, we are ready to begin testing this equation against different initial conditions. Before doing so, let's take a look at the necessary arguments for Scipy's IVP solver. Their documentation states that the function call looks like

```python
solve_ivp(odefunc, tSpan, y0, args=None, tEval=None)
```

The function call takes other arguments which we will not utilize here. However, those listed above are explained below.

* `odefunc`: This represents the name of the function to be evaluated. The IVP solver will call this function to create the solution.
* `tSpan`: This is meant to be a two-tuple meant to represent the start and stop times.
* `y0`: The initial condition(s) of the system as a tuple or list
* `args`: A tuple of arguments meant to be passed on to the `odefunc`. Note: **they must be in the same order as they appear in the `odefunc`**.
* `tEval`: Used to specify specific times to evaluate.

Now that we're aware of these, let's solve the charging problem under various initial conditions.

```python
# Set the values of the variables
m = 1
c = 0.5
k = 2
gamma = c/(2*m)
omega0Sq = k/m

# Set the initial value of the position and velocity
X0 = [1, 0]  # pos = 1, vel = 0

# Set Time Specs
tSpan = (0, 10)  # The simulation times for our system
tEval = np.linspace(tSpan[0], tSpan[1], 101)

# Solve the Problem
sol = ivp(dshm, tSpan, X0, args=(gamma, omega0Sq), t_eval=tEval)

# Plot the Solution
fig, ax = plt.subplots(figsize=(10, 7))
_ = ax.plot(sol.t, sol.y[0], linewidth=2, label='Position (m)')    # Plot the Position (which is the zeroth solution in the solution list)
_ = ax.plot(sol.t, sol.y[1], linewidth=2, label='Velocity (m/s)')  # Plot the Velocity (which is the first solution in the solution list)
_ = ax.set_xlim(tSpan)
_ = ax.set_xlabel('Time (s)',     fontsize=14)
_ = ax.set_ylabel('Amplitude', fontsize=14)
_ = ax.set_title('Damped Harmonic Oscillator', fontsize=16)
_ = ax.grid(True)
_ = ax.legend()
```

Now, let's instead assume that the mass starts at the origin and is given some initial kick in the positive direction.

```python
# Set the values of the variables
m = 1
c = 0.5
k = 2
gamma = c/(2*m)
omega0Sq = k/m

# Set the initial value of the position and velocity
X0 = [0, 2]  # pos = 0, vel = 2

# Set Time Specs
tSpan = (0, 10)  # The simulation times for our system
tEval = np.linspace(tSpan[0], tSpan[1], 101)

# Solve the Problem
sol = ivp(dshm, tSpan, X0, args=(gamma, omega0Sq), t_eval=tEval)

# Plot the Solution
fig, ax = plt.subplots(figsize=(10, 7))
_ = ax.plot(sol.t, sol.y[0], linewidth=2, label='Position (m)')    # Plot the Position (which is the zeroth solution in the solution list)
_ = ax.plot(sol.t, sol.y[1], linewidth=2, label='Velocity (m/s)')  # Plot the Velocity (which is the first solution in the solution list)
_ = ax.set_xlim(tSpan)
_ = ax.set_xlabel('Time (s)',     fontsize=14)
_ = ax.set_ylabel('Amplitude', fontsize=14)
_ = ax.set_title('Damped Harmonic Oscillator', fontsize=16)
_ = ax.grid(True)
_ = ax.legend()
```

Finally, let's take a look at the *critically damped* case when $\gamma = \omega_0$.

```python
# Set the values of the variables
gamma = 1
omega0 = 1
omega0Sq = omega0**2

# Set the initial value of the position and velocity
X0 = [1, -3]  # pos = 1, vel = -1

# Set Time Specs
tSpan = (0, 10)  # The simulation times for our system
tEval = np.linspace(tSpan[0], tSpan[1], 101)

# Solve the Problem
sol = ivp(dshm, tSpan, X0, args=(gamma, omega0Sq), t_eval=tEval)

# Plot the Solution
fig, ax = plt.subplots(figsize=(10, 7))
_ = ax.plot(sol.t, sol.y[0], linewidth=2, label='Position (m)')    # Plot the Position (which is the zeroth solution in the solution list)
_ = ax.plot(sol.t, sol.y[1], linewidth=2, label='Velocity (m/s)')  # Plot the Velocity (which is the first solution in the solution list)
_ = ax.set_xlim(tSpan)
_ = ax.set_xlabel('Time (s)',     fontsize=14)
_ = ax.set_ylabel('Amplitude', fontsize=14)
_ = ax.set_title('Critically Damped Harmonic Oscillator', fontsize=16)
_ = ax.grid(True)
_ = ax.legend()
```

You are welcome to play around with the three cells above to see how changing $k$, $m$, $c$ or the initial conditions affects the system.

### Coupled, Damped Harmonic Oscillators

Now, let's consider the case where we have a mass connected to a spring which is attached to the left wall and a spring attached between another mass and the right wall with a spring connecting the two masses in between. We can write the equations of motion for the system as being

\begin{align}
    m_1 \ddot{x}_1 &= -c \dot{x}_1 - k_1 x_1 - k_2 \left( x_1 - x_2 \right) \\
    m_2 \ddot{x}_2 &= -c \dot{x}_2 - k_2 \left( x_2 - x_1 \right) - k_3 x_2
\end{align}

we can then rewrite this as

\begin{align}
    \dot{x}_1 &= v_1 \\
    \dot{x}_2 &= v_2 \\
    \dot{v}_1 &= -2\gamma v_1 - \omega_0^2 x_1 - \omega_0^2 \left( x_1 - x_2 \right) \\
    \dot{v}_2 &= -2\gamma v_2 - \omega_0^2 \left( x_2 - x_1 \right) - \omega_0^2 x_2
\end{align}

assuming $k_1=k_2=k$ and $m_1=m_2=m$.

```python
# The Predator-Prey Equation
def cdhm(t, X, gamma, omega0Sq):
    '''A function for the ODE solver that specifies the conditions for damped, simple harmonic oscillations.'''

    # Unpack the variables
    pos1, pos2, vel1, vel2 = X

    # Return the Output of the Equations
    return [
        vel1,
        vel2,
        -2*gamma*vel1 - omega0Sq*pos1 - omega0Sq*(pos1 - pos2),
        -2*gamma*vel2 - omega0Sq*(pos2 - pos1) - omega0Sq*pos2
    ]
```

```python
# Set the values of the variables
m = 1
c = 0.5
k = 2
gamma = c/(2*m)
omega0Sq = k/m

# Set the initial value of the position and velocity
X0 = [0, -1, 0, 0]  # pos1 = 1, pos2 = 1.5, vel = 0

# Set Time Specs
tSpan = (0, 10)  # The simulation times for our system
tEval = np.linspace(tSpan[0], tSpan[1], 301)

# Solve the Problem
sol = ivp(cdhm, tSpan, X0, args=(gamma, omega0Sq), t_eval=tEval)

# Plot the Solution
fig, ax = plt.subplots(2, 1, figsize=(10, 12), sharex=True)
_ = ax[0].plot(sol.t, sol.y[0], linewidth=2, label='$x_1$')  # Plot the Position1 (which is the zeroth solution in the solution list)
_ = ax[0].plot(sol.t, sol.y[1], linewidth=2, label='$x_2$')  # Plot the Position2 (which is the first solution in the solution list)
_ = ax[1].plot(sol.t, sol.y[2], linewidth=2, label='$v_1$')  # Plot the Position1 (which is the zeroth solution in the solution list)
_ = ax[1].plot(sol.t, sol.y[3], linewidth=2, label='$v_2$')  # Plot the Position2 (which is the first solution in the solution list)
_ = ax[0].set_xlim(tSpan)
_ = ax[1].set_xlim(tSpan)
_ = ax[1].set_xlabel('Time (s)',  fontsize=14)
_ = ax[0].set_ylabel('Position (m)',   fontsize=14)
_ = ax[1].set_ylabel('Velocity (m/s)', fontsize=14)
_ = fig.suptitle('Coupled, Damped Harmonic Oscillator', fontsize=16)
_ = ax[0].grid(True)
_ = ax[1].grid(True)
_ = ax[0].legend()
_ = ax[1].legend()
```

```python
# Set the values of the variables
m = 1
c = 0.5
k = 2
gamma = c/(2*m)
omega0Sq = k/m

# Set the initial value of the position and velocity
X0 = [0, 0, 0, -2]  # pos1 = 1, pos2 = 1.5, vel = 0

# Set Time Specs
tSpan = (0, 10)  # The simulation times for our system
tEval = np.linspace(tSpan[0], tSpan[1], 301)

# Solve the Problem
sol = ivp(cdhm, tSpan, X0, args=(gamma, omega0Sq), t_eval=tEval)

# Plot the Solution
fig, ax = plt.subplots(2, 1, figsize=(10, 12), sharex=True)
_ = ax[0].plot(sol.t, sol.y[0], linewidth=2, label='$x_1$')  # Plot the Position1 (which is the zeroth solution in the solution list)
_ = ax[0].plot(sol.t, sol.y[1], linewidth=2, label='$x_2$')  # Plot the Position2 (which is the first solution in the solution list)
_ = ax[1].plot(sol.t, sol.y[2], linewidth=2, label='$v_1$')  # Plot the Position1 (which is the zeroth solution in the solution list)
_ = ax[1].plot(sol.t, sol.y[3], linewidth=2, label='$v_2$')  # Plot the Position2 (which is the first solution in the solution list)
_ = ax[0].set_xlim(tSpan)
_ = ax[1].set_xlim(tSpan)
_ = ax[1].set_xlabel('Time (s)',  fontsize=14)
_ = ax[0].set_ylabel('Position (m)',   fontsize=14)
_ = ax[1].set_ylabel('Velocity (m/s)', fontsize=14)
_ = fig.suptitle('Coupled, Damped Harmonic Oscillator', fontsize=16)
_ = ax[0].grid(True)
_ = ax[1].grid(True)
_ = ax[0].legend()
_ = ax[1].legend()
```

## Assignment

### The Pendulum

The equation of motion for a damped pendulum is

\begin{equation}
    l \ddot{\theta} + c \dot{\theta} + g \sin \theta = 0
\end{equation}

which can be rewritten as

\begin{align}
    \dot{\theta} &= \omega \\
    \dot{\omega} &= -\frac{c}{l} \omega - \frac{g}{l} \sin \theta
\end{align}

Write the function for Scipy below.

```python
def dpen(t, X, c, l, g):
    """Damped Pendulum"""

    # Unpack
    theta, omega = X

    # Return Solutions
    return [
        omega,
        -(c/l)*omega - (g/l)*np.sin(theta)
    ]
```

Write the code to solve the equation here for the initial conditions

\begin{align}
    \theta(0) &= \frac{9\pi}{10} \\
    \dot{\theta}(0) &= 0
\end{align}

and when $c=0.5$, $g=9.8$ and $l=1$.

```python
# Set the values of the variables
c = 0.5
g = 9.8
l = 1.0

# Set the initial value of the position and velocity
X0 = [9*np.pi/10, 0]  # pos = 0, vel = 2

# Set Time Specs
tSpan = (0, 10)  # The simulation times for our system
tEval = np.linspace(tSpan[0], tSpan[1], 301)

# Solve the Problem
sol = ivp(dpen, tSpan, X0, args=(c, l, g), t_eval=tEval)

# Plot the Solution
fig, ax = plt.subplots(figsize=(10, 7))
_ = ax.plot(sol.t, sol.y[0], linewidth=2, label='$\theta$ (rad)')    # Plot the Position (which is the zeroth solution in the solution list)
_ = ax.plot(sol.t, sol.y[1], linewidth=2, label='$\omega$ (rad/s)')  # Plot the Velocity (which is the first solution in the solution list)
_ = ax.set_xlim(tSpan)
_ = ax.set_xlabel('Time (s)',     fontsize=14)
_ = ax.set_ylabel('Amplitude', fontsize=14)
_ = ax.set_title('Damped Pendulum', fontsize=16)
_ = ax.grid(True)
_ = ax.legend()
```

### Coupled Pendulums

In the case when two pendula are connected to each other via a spring, the equations of motion are

\begin{align}
    \ddot{\theta}_1 + \frac{g}{l} \sin \theta_1 + \frac{k}{m} (\theta_1 - \theta_2) &= 0 \\
    \ddot{\theta}_2 + \frac{g}{l} \sin \theta_2 + \frac{k}{m} (\theta_2 - \theta_1) &= 0 \\
\end{align}

which can be written as

\begin{align}
    \dot{\theta}_1 &= \omega_1 \\
    \dot{\theta}_2 &= \omega_2 \\
    \dot{\omega}_1 &= - \frac{g}{l} \sin \theta_1 - \frac{k}{m} (\theta_1 - \theta_2) \\
    \dot{\omega}_2 &= - \frac{g}{l} \sin \theta_2 - \frac{k}{m} (\theta_2 - \theta_1) \\
\end{align}

Write the function for Scipy below.

```python
def cpen(t, X, g, l, k, m):
    """Coupled Pendulums"""

    # Unpack
    th1, th2, om1, om2 = X

    # Return Solutions
    return [
        om1,
        om2,
        -(g/l)*np.sin(th1) - (k/m)*(th1 - th2),
        -(g/l)*np.sin(th2) - (k/m)*(th2 - th1),
    ]
```

And write the code to solve the differential equation below for the initial conditions

\begin{align}
    \theta_1(0) &= 0 \\
    \theta_2(0) &= \frac{\pi}{6} \\
    \dot{\theta}_1(0) &= 0 \\
    \dot{\theta}_2(0) &= 0 \\
\end{align}

and when $k=1$, $m=1$, $g=9.8$ and $l=2$.

```python
# Set the values of the variables
g = 9.8
l = 1
k = 1
m = 1

# Set the initial value of the position and velocity
X0 = [0, np.pi/6, 0, 0]

# Set Time Specs
tSpan = (0, 30)  # The simulation times for our system
tEval = np.linspace(tSpan[0], tSpan[1], 1001)

# Solve the Problem
sol = ivp(cpen, tSpan, X0, args=(g, l, k, m), t_eval=tEval)

# Plot the Solution
fig, ax = plt.subplots(2, 1, figsize=(10, 12), sharex=True)
_ = ax[0].plot(sol.t, sol.y[0], linewidth=2, label='$x_1$')  # Plot the Position1 (which is the zeroth solution in the solution list)
_ = ax[0].plot(sol.t, sol.y[1], linewidth=2, label='$x_2$')  # Plot the Position2 (which is the first solution in the solution list)
_ = ax[1].plot(sol.t, sol.y[2], linewidth=2, label='$v_1$')  # Plot the Position1 (which is the zeroth solution in the solution list)
_ = ax[1].plot(sol.t, sol.y[3], linewidth=2, label='$v_2$')  # Plot the Position2 (which is the first solution in the solution list)
_ = ax[0].set_xlim(tSpan)
_ = ax[1].set_xlim(tSpan)
_ = ax[1].set_xlabel('Time (s)',  fontsize=14)
_ = ax[0].set_ylabel('Angle (rad)',   fontsize=14)
_ = ax[1].set_ylabel('Angular Velocity (rad/s)', fontsize=14)
_ = fig.suptitle('Coupled, Damped Harmonic Oscillator', fontsize=16)
_ = ax[0].grid(True)
_ = ax[1].grid(True)
_ = ax[0].legend()
_ = ax[1].legend()
```


__Problem 1__

__Proof:__
By Taylor's Formula, we have

\begin{equation}
\begin{aligned}
f(x + 2h) &= f(x) + f'(x) \cdot 2h + f''(x) \cdot \frac{(2h)^2}{2} + f'''(x) \cdot \frac{(2h)^3}{6} + f^{(4)}(x) \cdot \frac{(2h)^4}{24} + O(h^5) \\
&= f(x) + f'(x) \cdot 2h + f''(x) \cdot 2h^2 + f'''(x) \cdot \frac{4h^3}{3} + f^{(4)}(x) \cdot \frac{2h^4}{3} + O(h^5) \\
f(x + h) &= f(x) + f'(x) \cdot h + f''(x) \cdot \frac{h^2}{2} + f'''(x) \cdot \frac{h^3}{6} + f^{(4)}(x) \cdot \frac{h^4}{24} + O(h^5) \\
f(x - h) &= f(x) - f'(x) \cdot h + f''(x) \cdot \frac{h^2}{2} - f'''(x) \cdot \frac{h^3}{6} + f^{(4)}(x) \cdot \frac{h^4}{24} + O(h^5) \\
f(x - 2h) &= f(x) - f'(x) \cdot 2h + f''(x) \cdot 2h^2 - f'''(x) \cdot \frac{4h^3}{3} + f^{(4)}(x) \cdot \frac{2h^4}{3} + O(h^5) \\
\end{aligned}
\end{equation}

To approximate the ﬁrst derivative $u'(x)$ of a very smooth function with an error of only $O(h^4)$, we need to show that there exists a linear combination of $f(x + 2h),f(x + h),f(x - h),f(x - 2h),$ which eliminates the terms: $f''(x),f'''(x),f^{(4)}(x)$ as well as making the coefficient of the term: $f'(x)$ equals to $1$.

Thus we need to solve the following quaternion equations:

\begin{equation}
\left\{
  \begin{array}{**lr**}
  a - b + 2c - 2d = 1, \\
  \frac{1}{2}a + \frac{1}{2}b + 2c + 2d = 0, \\
  \frac{1}{6}a - \frac{1}{6}b + \frac{4}{3}c - \frac{4}{3}d = 0, \\
  \frac{1}{24}a + \frac{1}{24}b + \frac{2}{3}c + \frac{2}{3}d = 0   .
  \end{array}
\right.
\end{equation}

After calculation, we have :
$$[a,b,c,d] = [\frac{2}{3},-\frac{2}{3},-\frac{1}{12},\frac{1}{12}]$$
Thus, we have:
\begin{eqnarray}
|\frac{2}{3}f(x + h) - \frac{2}{3}f(x - h) - \frac{1}{12}f(x + 2h) + \frac{1}{12}f(x - 2h)| = f'(x) \cdot h + O(h^5) \\
|\frac{\frac{2}{3}f(x + h) - \frac{2}{3}f(x - h) - \frac{1}{12}f(x + 2h) + \frac{1}{12}f(x - 2h) - f'(x)}{h}| = O(h^4) \\
\end{eqnarray}

```
import numpy as np
from scipy.linalg import solve
a = np.array([[1,-1,2,-2],[1/2,1/2,2,2],[1/6,-1/6,4/3,-4/3],[1/24,1/24,2/3,2/3]])
b = np.array([1,0,0,0])
x = solve(a, b)
print(x)

```

    [ 0.66666667 -0.66666667 -0.08333333  0.08333333]

__Problem 2__

- __Pf:__
Since $f(x)$ is a smooth even function, we have $$f(x) = f(-x).$$

Then we have $$f'(x) = f'(-x) = (-x)' \cdot f'(-x) = -f'(-x),$$

thus $$f'(x) + f'(-x) = 0,$$

when $x = 0,$ we have $$f'(0) = 0.$$

__Q.E.D__

- __Pf:__
As similar to the previous proof, we have $f'''(x) = 0,$

By Taylor's Formula, we have
\begin{equation}
\begin{split}
f(h) &= f(0) + f'(0) \cdot h + f''(0) \cdot \frac{h^2}{2} + f'''(0) \cdot \frac{h^3}{6} + O(h^4) \\
&= f''(0) \cdot \frac{h^2}{2} + O(h^4). \\
\end{split}
\end{equation}
Thus, we will have $$|a_h - f''(0)| = |\frac{2f(h)}{h^2} - f''(0)| = O(h^2).$$
__Q.E.D__

- __Soln:__
As similar to the previous proof, we have $f^{(5)}(x) = 0,$

By Taylor's Formula, we have
\begin{equation}
\begin{split}
f(h) &= f''(0) \cdot \frac{h^2}{2} + f^{(4)} \cdot \frac{h^4}{24} + O(h^6). \\
f(2h) &= f''(0) \cdot \frac{(2h)^2}{2} + f^{(4)} \cdot \frac{(2h)^4}{24} + O(h^6) \\
&= f''(0) \cdot 2h^2 + f^{(4)} \cdot \frac{2h^4}{3} + O(h^6). \\
\end{split}
\end{equation}

To let $$|b_h - f''(x)| = |\frac{c_1 \cdot f(h) + c_2 \cdot f(2h)}{h^2} - f''(x)| = O(h^2),$$

we have the following binary linear equations：

\begin{equation}
\left\{
  \begin{array}{**lr**}
  \frac{c_1}{2} +2c_2 = 1, \\
  \frac{c_1}{24} + \frac{2}{3}c_2 = 0   .
  \end{array}
\right.
\end{equation}

thus $c_1 = \frac{8}{3}, c_2 = -\frac{1}{6}$.

- __Soln:__
Since $f(x)$ is a smooth odd function, we have $$f(x) = -f(-x).$$

Then we have $$f'(x) = (-f(-x))' = (-x)' \cdot -f'(-x) = f'(-x),$$

then taking this step again, we have $$f''(x) = (f'(-x))' = (-x)' \cdot (f''(-x)) = -f''(x),$$

thus $$f''(x) + f''(-x) = 0,$$

when $x = 0,$ we have $$f''(0) = 0.$$

```
import numpy as np
from scipy.linalg import solve
a = np.array([[1/2,2],[1/24,2/3]])
b = np.array([1,0])
x = solve(a, b)
print(x)
```

    [ 2.66666667 -0.16666667]