 Text provided under a Creative Commons Attribution license, CC-BY. All code is made available under the FSF-approved MIT license. (c) Kyle T. Mandli

Material heavily borrowed from David I. Ketcheson's [HyperPython](https://github.com/ketch/HyperPython) under a similar license as the one above.  Mistakes introduced are the responsibility of Kyle Mandli so complain to him.

If you are interested in the animation tools used there is a tutorial on their use at

https://nbviewer.jupyter.org/url/www.clawpack.org/_static/notebooks/animation_tools_demo.ipynb

```python
%matplotlib inline

from __future__ import print_function
import numpy
import matplotlib.pyplot as plt

# Note that this module is found in the base directory
import utils.animation as animation
```

# Finite Volume Methods

## Hyperbolic Conservation Laws

Before we specifically discuss finite volume methods we will explore the class of PDEs that are *hyperbolic conservation laws*.  These equations generally describe the movement of some physical quantity that should be conserved.  Examples of these quantities include mass, momentum, or energy.

### Conservation of Mass

Imagine a fluid flowing in a narrow tube.  We'll use $q$ to indicate the density of the fluid and $u$ to indicate its velocity.  Both of these are functions of space and time: $q = q(x,t)$; $u=u(x,t)$.  The total mass in the section of tube $[x_1,x_2]$ is

\begin{equation}
\int_{x_1}^{x_2} q(x,t) dx.
\end{equation}

This total mass can change in time due to fluid flowing in or out of this section of the tube.  We call the rate of flow the *flux*, and represent it with the function $f(q)$.  Thus the net rate of flow of mass into (or out of) the interval $[x_1,x_2]$ at time $t$ is

$$f(q(x_1,t)) - f(q(x_2,t)).$$

We just said that this rate of flow must equal the time rate of change of total mass; i.e.
$$
    \frac{d}{dt} \int_{x_1}^{x_2} q(x,t) dx = f(q(x_1,t)) - f(q(x_2,t)).
$$

Assuming the functions are continuous (enough) we can write
$$
    \int_{x_1}^{x_2} \frac{\partial}{\partial x} f(q) dx = f(q(x_2,t)) - f(q(x_1,t))
$$
and therefore rewrite out conservation law as
$$
    \frac{d}{dt} \int_{x_1}^{x_2} q(x,t) dx = -\int_{x_1}^{x_2} \frac{\partial}{\partial x} f(q) dx.
$$

Under certain smoothness assumptions on $q$, we can move the time derivative inside the integral.  We'll also put everything on the left side, to obtain
$$
    \int_{x_1}^{x_2} \left(\frac{\partial}{\partial t}q(x,t) + \frac{\partial}{\partial x} f(q)\right) dx = 0.
$$

Since this integral is zero for *any* choice of $x_1,x_2$, it must be that the integrand (the expression in parentheses) is actually zero *everywhere*!  Therefore we can write the **differential conservation law**
$$
    q(x,t)_t + f(q(x,t))_x = 0.
$$

This equation expresses the fact that the total mass is conserved -- since locally the mass can change only due to a net inflow or outflow.

### Advection

In order to solve the conservation law above, we need an expression for the flux, $f$.  The rate of flow is just mass times velocity: $f=u q$.  Thus we obtain the **continuity equation**
$$
    q_t + (uq)_x = 0.
$$

In general, we need another equation to determine the velocity $u(x,t)$.  Later we will look at the full equations of fluid dynamics, but for now let's consider the simplest case, in which all of the fluid flows at a single, constant velocity $u(x,t)=a$.  Then the continuity equation becomes the *advection equation*

$$q_t + a q_x = 0.$$

We already saw that the solution to the advection equation is $$q(x,t) = q_0(x-at).$$  Let's plot the solution of the advection equation on the interval $[0,1]$ for the initial condition $$q_0(x) = e^{-2(x-1/2)^2}.$$

```python
# Grid
x = numpy.linspace(0, 1, 1000)

# Advection speed
a = 1.0

# Initial condition
def q_0(x):
    return numpy.exp(-200.*(x-0.2)**2)

figs = []
for t in numpy.linspace(0,1):
    figs.append(plt.figure(figsize=(8, 4)))
    axes = figs[-1].add_subplot(1, 1, 1)
    axes.plot(x, q_0(x - a * t), linewidth=2)
    axes.set_xlim((0, 1))
    axes.set_ylim((-0.1, 1.1))
    axes.set_xlabel("$x$")
    axes.set_ylabel("$q(x,t)$")
    axes.set_title("Solution to the advection equation")
    plt.close(figs[-1])

# Animate the solution
images = animation.make_images(figs)
animation.JSAnimate_images(images)
```

As you can see, the initial pulse just moves to the right at speed $a$ as time advances.  This isn't very interesting, but it captures the most important feature of hyperbolic equations: waves travel at finite speed.

### Finite Volume Method for Advection

We can easily solve the advection equation exactly.  But the advection equation is a prototype for more complicated conservation laws that we will only be able to solve approximately by using numerical methods.  In order to better understand these methods, we will discuss them first in the context of the advection equation.

For simplicity, we'll suppose that we wish to solve the advection equation on the interval $[0,1]$.  We introduce a set of equally spaced *grid cells* of width $\Delta x$, and write $x_i$ to mean the center of cell $i$.  Thus the first cell is the interval $[0,\Delta x]$ and $x_1=\Delta x/2$.  We will also write $x_{i-1/2}$ or $x_{i+1/2}$ to denote the left or right boundary of cell $i$, respectively.

We write $Q_i$ to denote the *average* value of the solution over cell $i$:

$$Q_i = \frac{1}{\Delta x} \int_{x_{i-1/2}}^{x_{i-1/2}} q \ dx.$$

The simplest finite volume method is obtained by supposing that the solution is actually *equal* to $Q_i$ over all of cell $i$.

Suppose $a>0$.  Then the flux into cell $i$ from the left is $a Q_{i-1}$ and the flux out of cell $i$ to the right is $a Q_i$.  Then our integral conservation law reads

$$Q_i'(t) = -\frac{a}{\Delta x}\left(Q_i - Q_{i-1}\right).$$

Applying a forward difference in time we obtain the *upwind method*
$$
    Q^{n+1}_i = Q^n_i -\frac{a \Delta t}{\Delta x}\left(Q_i - Q_{i-1}\right).
$$
This of course is also the same upwind method we derived when considering finite differences.

```python
a = 1.0

m = 100
delta_x = 1.0 / float(m)
x = numpy.arange(-delta_x / 2.0, 1.0 + delta_x / 2.0, delta_x)

t = 0.0
T = 0.5
CFL = 0.8
delta_t = CFL * delta_x / a

# Initial data
def q_0(x):
    return numpy.exp(-200.*(x-0.2)**2)
Q = q_0(x)
Q_new = Q.copy()

figs = []
while t < T:
    # Incoming wave
#     Q_new[0] = numpy.sin(20.0 * numpy.pi * t)
    # Extrapolation BCs
    Q_new[0] = Q_new[1]

    Q_new[1:] = Q[1:] - a * delta_t / delta_x * (Q[1:] - Q[:-1])

    Q = Q_new.copy()
    t = t + delta_t
    figs.append(plt.figure())
    axes = figs[-1].add_subplot(1, 1, 1)
    axes.plot(x, q_0(x - a * t), 'k', linewidth=2, label="True Solution")
    axes.plot(x, Q, 'o-', linewidth=2, label="Upwind")
    axes.set_title('t = %s' % str(t))
    axes.set_xlim((x[0], x[-1]))
    axes.set_ylim((-1.1, 1.1))
    axes.set_xlabel("$x$")
    axes.set_ylabel("$q(x,t)$")
    axes.set_title("Upwind Method")
    axes.legend(loc=4)
    plt.close(figs[-1])

# Animate the solution
images = animation.make_images(figs)
animation.JSAnimate_images(images)
```

We can see that we have picked a $\Delta t$ based on the CFL condition we derived for finite difference methods.  The same applies here (we are even using the same method).

Notice how we set up a grid that contains an extra cell at each end, outside of the problem domain $[0,1]$.  These are called *ghost cells* and are often useful in handling the solution at the grid boundaries.

The technique we have used to set the ghost cell values above, by copying the last value inside the grid to the ghost cells, is known as *zero-order extrapolation*.  It is useful for allowing waves to pass out of the domain (so-called *non-reflecting* boundaries).  Note that we don't actually need the ghost cell at the right end for the upwind method, but for other methods we will.

The upwind method is simple, but it is not very accurate.  Notice how the computed solution becomes wider and shorter over time.  This behavior is referred to as *dissipation*.

#### Exercises

Now do the following with the code above:

1. Set $m=1000$ or more and notice that it takes some time to compute the solution.  Rewrite the inner loop (over $i$) as a single line with no loop, using numpy slicing.  For large values of $m$, the code with slicing is much faster.
1. Notice that the last step of the simulation goes past time $T$.  Modify the code so that the last step is adjusted to exactly reach $T$.
2. Change the code so that animation of the solution versus time is plotted.  You will want to accumulate frames of the solution in a list and then use the same kind of code we used above to animate the exact solution.
3. Add some code to plot the exact solution.
4. Change the left boundary condition so that there is a sinusoidal wave coming in from the left:
$$u(0,t) = \sin(20 \pi t).$$
What do you notice about the sinusoid as it moves into the domain?

## Alternative Derivations

Another way to look view finite volume methods is more directly as a discretization of the PDE:
$$
    \frac{d}{dt} \int_{x_1}^{x_2} q(x,t) dx = f(q(x_1,t)) - f(q(x_2,t)).
$$
to
$$
    Q_i^{n+1} = Q^n_i - \frac{\Delta t}{\Delta x} \left [ F^n_{i+1/2} - F^n_{i-1/2} \right]
$$

If the flux from the left of cell $i$ says that the value $Q^n_{i-1}$ moves in at a speed $\lambda$ then it will cover $\lambda \Delta t$ of the total call $i$'s length leaving $\Delta x - \lambda \Delta t$ the original value $Q^n_i$.  Averaging these two values to find $Q^{n+1}_{i}$ leads to

$$\begin{aligned}
    Q^{n+1}_i &= \frac{Q^n_i (\Delta x - \lambda \Delta t) + Q^n_{i-1} \lambda \Delta t}{\Delta x} \\
    &=Q^n_i - \frac{\lambda \Delta t}{\Delta x} [Q^n_i - Q^n_{i-1}],
\end{aligned}$$

exactly the up-wind method!

## Traffic Flow

As an illustrative example we will now consider a particular scalar conservation law, that of a simplified model for traffic flow.  This model is sometimes referred to as the Lighthill-Whitham-Richards (or LWR) traffic model.

From the continuity equation above we know:

$$q_t + (uq)_x = 0.$$

Now we will think of $q$ as the density of cars on a road, traveling with velocity $u$.  Note that we're not keeping track of the individual cars, but just of the average number of cars per unit length of road.  Thus $q=0$ represents an empty stretch of road, and we can choose the units so that $q=1$ represents bumper-to-bumper traffic.

We'll also choose units so that the speed limit is $u_\text{max}=1$, and assume that drivers never go faster than this (yeah, right!)  If we assume that drivers always travel at a single uniform velocity, we obtain once again the advection equation.  But we all know that's not accurate in practice -- cars go faster in light traffic and slower when there is congestion.  The simplest way to incorporate this effect is to make the velocity a linearly decreasing function of the density:

$$u(q) = 1 - q.$$

Notice that $u$ goes to zero as $q$ approaches the maximum density of 1, while $u$ goes to the maximum value of 1 as traffic density goes to zero.  Obviously, both $q$ and $u$ should always stay in the interval $[0,1]$.

Combining the two equations above, our conservation law says

$$q_t + (q (1-q))_x = 0.$$

The function $q(1-q)$ is the flux, or the rate of flow of cars.  Notice how the flux is zero when there are no cars and also when the road is completely full.  The maximum flow of traffic actually occurs when the road is half full:

```python
q = numpy.linspace(0, 1)
f = q * (1.0 - q)
fig = plt.figure()
axes = fig.add_subplot(1, 1, 1)
axes.plot(q,f,linewidth=2)
axes.set_xlabel('q')
axes.set_ylabel('$f(q)$')
axes.set_xlim((0, 1))
axes.set_ylim((0.0, 0.3))
plt.show()
```

This equation is fundamentally different from the advection equation because the flux is **nonlinear**.  This fact will have dramatic consequences for both the behavior of solutions and our numerical methods.  But we can superficially make this equation look like the advection equation by using the chain rule to write

$$f(q)_x = f'(q) q_x = (1-2q)q_x.$$

Then we have

$$q_t + (1-2q)q_x = 0.$$

This is like the advection equation, but with a velocity $1-2q$ that depends on the density of cars.  The value $f'(q)=1-2q$ is referred to as the *characteristic speed*.  This characteristic speed is not the speed at which cars move (notice that it can even be negative!)  Rather, it is the speed at which *information* is transmitted along the road.

### Shock waves (traffic jams)

Let's use the *Lax-Friedrichs method* to solve the LWR traffic model.

```python
m = 100
delta_x = 1.0 / m
x = numpy.arange(-delta_x / 2.0, 1.0 + delta_x / 2.0, delta_x)

t = 0.0
T = 0.5
CFL = 0.9
delta_t = CFL * delta_x

Q = 0.9 * numpy.exp(-100.0 * (x - 0.5)**2)
Q_new = numpy.empty(Q.shape)
Q_list = [Q]

figs = []
while t < T:

    Q_new[1:-1] = 0.5 * (Q[:-2] + Q[2:]) - 0.5 * delta_t / delta_x \
                                               * (Q[2:] * (1.0 - Q[2:]) - Q[:-2] * (1.0 - Q[:-2]))

    Q = Q_new.copy()
    Q[0] = Q[-2]
    Q[-1] = Q[1]
    t += delta_t

    figs.append(plt.figure())
    axes = figs[-1].add_subplot(1, 1, 1)
    axes.plot(x, Q, 'o-', linewidth=2)
    axes.set_title('Lax-Friedrichs Method - Traffic Flow at t = %s' % str(t))
    axes.set_xlabel("$x$")
    axes.set_ylabel("$q(x,t)$")
    axes.set_xlim((x[0], x[-1]))
    axes.set_ylim((-0.1, 1.1))
    plt.close(figs[-1])

# Animate the solution
images = animation.make_images(figs)
animation.JSAnimate_images(images)
```

Starting from an initially smooth distribution of cars, we see that the solution evolves into something discontinuous!  What's going on?

Naturally, the cars in the low-density region behind the peak initially drive faster than the cars in the most congested region, ahead of them.  This leads to a steepening of the rear part of the traffic flow, and eventually there is a traffic jam: cars coming from the back have an open road, until suddenly they reach a highly congested area and must slam on the brakes.  Sound familiar?

#### Exercises

1. Recall our discussion of the CFL condition for advection.  To apply the CFL condition to a nonlinear problem, we use the characteristic velocity (in this case $1-2q$) in place of the velocity $a$.  For this problem, how large can we take the time step without violating the CFL condition?

1. In the cell below, set things up to find out what happens when cars reach a red light.  Place the red light at $x=1$ and set $q=1$ in the interval $0.8<x<1$ near the light (the cars will be stopped, bumper to bumper there).  In the rest of the domain, set $q=0.2$, corresponding to light traffic.  In the ghost cell at the right boundary, set $q=1$; in the ghost cell at the left set $q=0.2$.  After you have computed the solution, check it against your inuition.  Does it make sense?

### Speed of a shock wave: the Rankine-Hugoniot conditions

In the second exercise above, you should have seen a shock wave (i.e., a discontinuity) that moves to the left as more and more cars pile up behind the red light.  How quickly does this discontinuity move to the left?

We can figure it out by putting an imaginary line at the location of the shock.  Let $q_l$ be the density of cars just to the left of the line, and let $q_r$ be the density of cars just to the right.  Imagine for a moment that the line is stationary.  Then the rate of cars reaching the line from the left is $f(q_l)$ and the rate of cars departing from the line to the right is $f(q_r)$.  If the line really were stationary, we would need to have $f(q_l)-f(q_r)=0$ to avoid cars accumulating at the line.

However, the shock is not stationary, so the line is moving.  Let $s$ be the speed of the shock.  Then as the line moves to the left, some cars that were to the left are now to the right of the line.  The rate of cars removed from the left is $s q_l$ and the rate of cars added on the right is $s q_r$.  So in order to avoid an accumulation of cars at the shock, these two effects need to be balanced:
$$
    f(q_l) - f(q_r) = s(q_l - q_r).
$$

This condition is known as the *Rankine-Hugoniot condition*, and it holds for any shock wave in the solution of any hyperbolic system.

Another more analytical way to get at the Rankine-Hugoniot conditions is to consider a rectangle $[x_1, x_1 + \Delta x] \times [t_1, t_1 + \Delta t]$ which is split by a shock with speed $s$.

Applying the integral conservation law to this region assuming that $q$ is constant in each region we have
$$\begin{aligned}
    \int^{x_1 + \Delta x}_{x_1} q(x, t_1 + \Delta t) dx - \int^{x_1 + \Delta x}_{x_1} q(x, t_1) dx &= \int^{t_1 + \Delta t}_{t_1} q(x_1, t) dt - \int^{t_1 + \Delta t}_{t_1} q(x_1 + \Delta x, t) dt \\
    \Delta x q_r - \Delta x q_\ell &= \Delta t f(q_\ell) - \Delta t f(q_r) + \mathcal{O}(\Delta t^2) \\
    \frac{\Delta x}{\Delta t} (q_r - q_\ell) &=  (f(q_\ell) -  f(q_r)) + \mathcal{O}(\Delta t)
\end{aligned}$$
then letting $\Delta t \rightarrow 0$ we have
$$
    s (q_r - q_\ell) =  f(q_r) -  f(q_\ell)
$$
noting that $-s \Delta t = \Delta x$.

#### Exercises

1. Recall our discussion of the CFL condition for advection.  To apply the CFL condition to a nonlinear problem, we use the characteristic velocity (in this case $1-2q$) in place of the velocity $a$.  For this problem, how large can we take the time step without violating the CFL condition?

1. In the cell below, set things up to find out what happens when cars reach a red light.  Place the red light at $x=1$ and set $q=1$ in the interval $0.8<x<1$ near the light (the cars will be stopped, bumper to bumper there).  In the rest of the domain, set $q=0.2$, corresponding to light traffic.  In the ghost cell at the right boundary, set $q=1$; in the ghost cell at the left set $q=0.2$.  After you have computed the solution, check it against your inuition.  Does it make sense?

### Rarefaction Waves

Why didn't we see a shock wave in the third exercise above?  Initially, the solution is discontinuous, but after time zero it becomes continuous.

Physically, the reason is clear: after the light turns green, the cars in front accelerate and spread out; then the cars behind them accelerate, and so forth.  This kind of expansion wave is referred to as a *rarefaction wave* by analogy to fluid dynamics.

How can we determine whether an initial discontinuity will lead to a shock or a rarefaction?
- Shocks appear in regions where characteristics overlap:

- Rarefactions appear in regions where characteristics are spreading out:

More precisely, if the value to the left of a shock is $q_l$ and the value to the right is $q_r$, then it must be that $f'(q_l)>f'(q_r)$.  In fact the shock speed must lie between these characteristic speeds:

$$f'(q_l) > s > f'(q_r).$$

We say that the characteristics *impinge* on the shock.  This is known as the *entropy condition*, because in fluid dynamics such a shock obeys the 2nd law of thermodynamics.

On the other hand, if $f'(q_l)< f'(q_r)$, then a rarefaction wave results.

### The Riemann Problem

The problems you set up in the red and green light exercises above are examples of what is known as a Riemann problem.  A Riemann problem consists of  a hyperbolic conservation law, together with piecewise-constant initial data with a single point of discontinuity:

$$q(x,0) = \begin{cases} q_l & x < x_0 \\ q_r & x > x_0 \end{cases}$$

In fact, our entire finite volume method is built on approximately solving Riemann problems.  Since the solution is approximated by a constant function in each cell, a Riemann problem arises at each interface between two cells.  The upwind method for advection is based on exactly solving the Riemann problem to find the flux (of course, the Riemann problem for advection is extremely simple to solve).  The Lax-Friedrichs flux is based on approximating the solution to the Riemann problem.

For nonlinear systems of hyperbolic PDEs, like the Euler equations that we will consider later, the solution of the Riemann problem can be quite complicated.  Computational methods for solving such systems are usually based on approximate Riemann solvers.  We won't go into the theory of Riemann problems or approximate Riemann solvers in this course.

## High-Resolution Methods

The methods we have used so far (the *upwind method* and the *Lax-Friedrichs method*) are both dissipative.  This means that over time they artificially smear out the solution -- especially shocks.  Furthermore, both of these methods are only *first order accurate*, meaning that if we reduce the values of $\Delta t$ and $\Delta x$ by a factor of two, the overall error decreases (only) by a factor of two.  We can do better.

### Reducing Diffusion (Dissipation)

The first step in improving our accuracy is to use a more accurate representation of $q(x)$ at each step.  Instead of assuming that $q$ is piecewise-constant, we will now approximate it by a piecewise-linear function:

$$q(x) = Q^n_i + \sigma^n_i (x-x_i).$$

Here $\sigma_i$ represents the slope in cell $i$.  The most obvious choice to ensure that this results in a second-order accurate approximation is to take the centered approximation

$$\sigma^n_i = \frac{Q^n_{i+1} - Q^n_{i-1}}{2\Delta x}.$$

We use this to obtain values at the cell interfaces:

$$\begin{aligned}
q^+_{i - 1/2} & = Q_i - \sigma_i \frac{\Delta x}{2} \\
q^-_{i + 1/2} & = Q_i + \sigma_i \frac{\Delta x}{2}.
\end{aligned}$$

We'll use these interface values to approximate the flux, based on the *Lax-Friedrichs flux* (related to the Lax-Friedrichs method):

$$F_{i - 1/2} = \frac{1}{2} \left( f(q^-_{i - 1/2}) + f(q^+_{i - 1/2}) - \frac{\Delta t}{\Delta x} (q^+_{i - 1/2} - q^-_{i - 1/2})\right)$$

This provides second-order accuracy in space.  We also need to make the method second-order accurate in time.  We can do so by using a second-order Runge--Kutta method.  Then the full method is

$$\begin{aligned}
Q^*_i & = Q^n_i - \frac{\Delta t}{\Delta x}\left( F^n_{i - 1/2} - F^n_{i - 1/2} \right) \\
Q^{n+1}_i & = \frac{1}{2} Q^n_i + \frac{1}{2}\left( Q^*_i - \frac{\Delta t}{\Delta x}\left( F^*_{i + 1/2} - F^*_{i - 1/2} \right) \right) \\
\end{aligned}$$

```python
def f(q):
    return q * (1.0 - q)

m = 100
delta_x = 1.0 / float(m)
x = numpy.arange(-3.0 * delta_x / 2.0, 1.0 + 5.0 * delta_x / 2.0, delta_x)

t = 0. # Initial time
T = 0.5 # Final time
CFL = 0.9
delta_t = CFL * delta_x  # Time step

Q_HR = 0.9 * numpy.exp(-100.0 * (x - 0.5)**2)
Q_new_HR = numpy.zeros(m + 4)
Q_star = numpy.zeros(m + 4)
sigma = numpy.zeros(m + 4)
F = numpy.zeros(m + 4)

Q_LF = 0.9 * numpy.exp(-100.0 * (x - 0.5)**2)
Q_new_LF = numpy.zeros(m + 4)

figs = []
while t < T:

    # High-resolution version
    sigma[1:-1]  = (Q_HR[2:] - Q_HR[:-2]) / (2.0 * delta_x)
    q_plus  = Q_HR[1:-1] - sigma[1:-1] * delta_x / 2.0
    q_minus = Q_HR[:-2] + sigma[:-2]  * delta_x / 2.0
    F[1:-1] = 0.5 * (f(q_plus) + f(q_minus) - delta_x / delta_t * (q_plus - q_minus) )

    Q_star[2:-2] = Q_HR[2:-2] - delta_t / delta_x * (F[3:-1] - F[2:-2])
    Q_star[0:2] = Q_star[2]
    Q_star[-2:] = Q_star[-3]

    sigma[1:-1]  = (Q_star[2:] - Q_star[:-2]) / (2.0 * delta_x)
    q_plus  = Q_star[1:-1] - sigma[1:-1] * delta_x / 2.0
    q_minus = Q_star[:-2] + sigma[:-2]  * delta_x / 2.0
    F[1:-1] = 0.5 * (f(q_plus) + f(q_minus) - delta_x / delta_t * (q_plus - q_minus))

    Q_new_HR[2:-2] = 0.5 * Q_HR[2:-2] + 0.5 * (Q_star[2:-2] - delta_t / delta_x * (F[3:-1] - F[2:-2]))

    Q_HR = Q_new_HR.copy()
    Q_HR[0:2] = Q_HR[2]
    Q_HR[-2:] = Q_HR[-3]
    t += delta_t

    # LF Version
    Q_new_LF[1:-1] = 0.5 * (Q_LF[:-2] + Q_LF[2:]) - 0.5 * delta_t / delta_x * (Q_LF[2:] *(1.0 - Q_LF[2:]) - Q_LF[:-2] * (1.0 - Q_LF[:-2]))

    Q_LF = Q_new_LF.copy()
    Q_LF[0] = Q_LF[-2]
    Q_LF[-1] = Q_LF[1]
    t += delta_t

    figs.append(plt.figure())
    axes = figs[-1].add_subplot(1, 1, 1)
    axes.plot(x, Q_HR, 'o-', linewidth=2, label="High Resolution")
    axes.plot(x, Q_LF, 'x', linewidth=2, label="Original")
    axes.set_title('t = %s' % str(t))
    axes.set_xlim((x[0], x[-1]))
    axes.set_ylim((-0.1, 1.1))
    axes.legend()
    plt.close(figs[-1])

# Animate the solution
images = animation.make_images(figs)
animation.JSAnimate_images(images)
```

The shock wave is much sharper now, but we have a new problem. Do you see the little dip behind the shock?  If you look closely, you'll see that the solution is actually negative there!  Obviously, a negative density of cars makes no sense.  What's more, the solution shouldn't dip there -- and it shouldn't have that funny bump just in front of the shock either.  What to do?

### Slope Limiting

The spurious oscillations in our solution are not a particular feature of the method we've chosen.  In fact, *any* second-order (or higher) method that computes $q^\pm_{i + 1/2}$ as a linear function of the cell averages will have oscillations (this is a famous result known as *Godunov's Theorem*).

We can get around this difficulty by choosing the slope $\sigma$ as a *nonlinear* function of the cell averages.  In particular, to avoid oscillations we can choose the smaller of the two one-sided slopes.  Let $\Delta Q_{i - \Delta x} = Q_i - Q_{i-1}$.  Then we use the slope

$$\begin{aligned}
\sigma_i & = \text{minmod}(\Delta Q_{i-1/2},\Delta Q_{i + 1/2})/\Delta x \\
& = \begin{cases} \min(\Delta Q_{i-1/2}, \Delta Q_{i + 1/2})/\Delta x & \text{ if } \Delta Q_{i-1/2}, \Delta Q_{i + 1/2} > 0 \\
\max(\Delta Q_{i - 1/2}, \Delta Q_{i + 1/2})/\Delta x & \text{ if } \Delta Q_{i-1/2}, \Delta Q_{i + 1/2} < 0 \\
0 & \text{ if } \Delta Q_{i-1/2}\cdot \Delta Q_{i + 1/2} < 0.
\end{cases}
\end{aligned}$$

This choice of slope is known as the minimum-modulus, or *minmod* slope.

### Local Lax-Friedrichs flux

The Lax-Friedrichs flux ensures that our solution is stable, but it does so by adding a lot of dissipation everywhere.  In fact, we could get away with using less dissipation over most of the domain.  A variant that does this is called the *local Lax-Friedrichs flux*.  It is little more accurate than the Lax-Friedrichs flux because it uses the local characteristic speeds to determine how much dissipation is needed at each interface.  It is

$$F_{i - \Delta x} = \frac{1}{2} \left( f(q^-_{i - 1/2}) + f(q^+_{i - 1/2}) - \alpha_{i - 1/2}\frac{\Delta x}{\Delta t} (q^+_{i - 1/2} - q^-_{i - 1/2})\right)$$

where

$$\alpha_{i - 1/2} = \min(\left|f'(q^-_{i - 1/2})\right|,\left|f'(q^+_{i - 1/2})\right|).$$

#### Exercise

In the cell below, copy and modify the second-order method above to use the minmod slope and local Lax-Friedrichs flux.

*Hint 1*: You will want to use the functions `numpy.minimum` and `numpy.maximum`, which compare two arrays elementwise (not `numpy.min`, which finds the minimum of a single array).

*Hint 2*: to avoid using a loop for the slope computation, note that the minmod function can be written as
$$
\text{minmod}(a,b) = \frac{\text{sign}(a)+\text{sign}(b)}{2} \min(|a|,|b|).
$$
The signum function is implemented as `numpy.sign()`.

```python
def minmod(a, b):
    return (numpy.sign(a) + numpy.sign(b)) / 2.0 * numpy.minimum(numpy.abs(a), numpy.abs(b))

def f(q):
    return q * (1.0 - q)

def f_prime(q):
    return 1.0 - 2.0 * q

m = 100
delta_x = 1.0 / float(m)
x = numpy.arange(-3.0 * delta_x / 2.0, 1.0 + 5.0 * delta_x / 2.0, delta_x)

t = 0. # Initial time
T = 0.5 # Final time
# T = delta_t * 20
CFL = 0.9
delta_t = CFL * delta_x  # Time step

Q = 0.9 * numpy.exp(-100.0 * (x - 0.5)**2)
Q_new = numpy.zeros(m + 4)
Q_star = numpy.zeros(m + 4)
sigma = numpy.zeros(m + 4)
F = numpy.zeros(m + 4)

Q_LF = 0.9 * numpy.exp(-100.0 * (x - 0.5)**2)
Q_new_LF = numpy.zeros(m + 4)

figs = []
while t < T:

    sigma[1:-1]  = minmod(Q[1:-1] - Q[:-2], Q[2:] - Q[1:-1]) / delta_x
    Q_plus  = Q[1:-1] - sigma[1:-1] * delta_x / 2.0
    Q_minus = Q[:-2] + sigma[:-2]  * delta_x / 2.0
    alpha = numpy.minimum(numpy.abs(f_prime(Q_minus)), numpy.abs(f_prime(Q_plus)))
    F[1:-1] = 0.5 * (f(Q_plus) + f(Q_minus) - alpha * delta_x / delta_t * (Q_plus - Q_minus) )

    Q_star[2:-2] = Q[2:-2] - delta_t / delta_x * (F[3:-1] - F[2:-2])
    Q_star[0:2] = Q_star[2]
    Q_star[-2:] = Q_star[-3]

    sigma[1:-1]  = (Q_star[2:] - Q_star[:-2]) / (2.0 * delta_x)
    q_plus  = Q_star[1:-1] - sigma[1:-1] * delta_x / 2.0
    q_minus = Q_star[:-2] + sigma[:-2]  * delta_x / 2.0
    F[1:-1] = 0.5 * (f(Q_plus) + f(Q_minus) - delta_x / delta_t * (Q_plus - Q_minus))

    Q_new[2:-2] = 0.5 * Q[2:-2] + 0.5 * (Q_star[2:-2] - delta_t / delta_x * (F[3:-1] - F[2:-2]))

    Q = Q_new.copy()
    Q[0:2] = Q[2]
    Q[-2:] = Q[-3]
    t += delta_t

    # LF Version
    Q_new_LF[1:-1] = 0.5 * (Q_LF[:-2] + Q_LF[2:]) - 0.5 * delta_t / delta_x * (Q_LF[2:] *(1.0 - Q_LF[2:]) - Q_LF[:-2] * (1.0 - Q_LF[:-2]))

    Q_LF = Q_new_LF.copy()
    Q_LF[0] = Q_LF[-2]
    Q_LF[-1] = Q_LF[1]
    t += delta_t

    figs.append(plt.figure())
    axes = figs[-1].add_subplot(1, 1, 1)
    axes.plot(x, Q, 'o-', linewidth=2, label="High Resolution")
    axes.plot(x, Q_LF, 'x', linewidth=2, label="Original")
    axes.set_title('t = %s' % str(t))
    axes.set_xlim((x[0], x[-1]))
    axes.set_ylim((-0.1, 1.1))
    axes.legend()
    plt.close(figs[-1])

# Animate the solution
images = animation.make_images(figs)
animation.JSAnimate_images(images)
```

As you can see, this method keeps the shock fairly sharp and avoids the creation of negative solution values.  This method falls into the class of [MUSCL]() schemes and is proven to avoid oscillations.

### High-Order Methods

Just like the method we implemented above, most methods that are more than first-order accurate consist of three components:
1. **Reconstruction**: A method for computing interface values $q^\pm_{i - \Delta x}$ from cell averages $Q_i$.  This involves some kind of limiting in order to avoid oscillations.  Higher-order reconstruction is often done using [weighted essentially non-oscillatory (WENO)](http://www.dam.brown.edu/scicomp/media/report_files/BrownSC-2006-21.ps.gz) methods.
2. **Numerical flux**:  An approximation of the flux, computed based on the interface values $q^\pm_{i - \Delta x}$.  The Lax-Friedrichs flux above is one of the simplest.  Much more accurate fluxes can be computed using an exact or approximate [Riemann solver](http://en.wikipedia.org/wiki/Riemann_solver).
3. **Time integrator**: In order to get high-order accuracy in time, usually a [Runge-Kutta method](http://en.wikipedia.org/wiki/Runge%E2%80%93Kutta_methods) is used.  [Strong stability preserving methods](http://www.davidketcheson.info/assets/papers/sspreview.pdf) are particularly popular.

It's possible to devise methods of very high order by increasing the order of the polynomial reconstruction and of the time integrator.

There are many different ways to reconstruct and limit solutions, try out a few.  This requires a Clawpack installation, see the [Clawpack webpage](http://www.clawpack.org) for installation instructions.

```python
# Demo limiters
from clawpack import pyclaw
from clawpack import riemann

# Pick out Riemann solver
riemann_solver = riemann.advection_1D_py.advection_1D

# Create solver
solver = pyclaw.ClawSolver1D(riemann_solver)
solver.kernel_language = "Python"
solver.bc_lower[0] = pyclaw.BC.periodic
solver.bc_upper[0] = pyclaw.BC.periodic

# Limiters avaiable
solver.limiters = 4
# To pick a Godunov upwind method set order to 1
solver.order = 2
limiter_name = ["None", "minmod", "superbee", "Van Leer", "MC", "Beam-Warming", "Fromm",
                "Albada 2", "Albada 3", "Van Leer with Klein sharpening"]

# Set up domain
x = pyclaw.Dimension(0.0, 1.0, 100, name='x')
domain = pyclaw.Domain(x)
state = pyclaw.State(domain, solver.num_eqn)

# Problem specific data
state.problem_data['u'] = 1.0

# Initial condition
xc = state.grid.x.centers
beta = 100; gamma = 0; x0 = 0.75
state.q[0, :] = numpy.exp(-beta * (xc-x0)**2) * numpy.cos(gamma * (xc - x0))
state.q[0, :] += 1.0 * (0.2 < xc) * (xc < 0.4)

# Set up time stepping and other run parameters
claw = pyclaw.Controller()
claw.keep_copy = True
claw.solution = pyclaw.Solution(state, domain)
claw.solver = solver
claw.num_output_times = 30
claw.tfinal = 30.0

claw.run()

figs = []
for frame in claw.frames:
    figs.append(plt.figure())
    axes = figs[-1].add_subplot(1, 1, 1)
    axes.plot(xc, frame.q[0, :], 'o-', linewidth=2)
    axes.plot(xc, claw.frames[0].q[0, :], 'k--')
    axes.set_xlim((0, 1))
    axes.set_ylim((-0.2, 1.2))
    axes.set_title("Limiter = %s" % limiter_name[solver.limiters])
    plt.close(figs[-1])

# Animate the solution
images = animation.make_images(figs)
animation.JSAnimate_images(images)
```

## Fluid Dynamics

As a more complex example of a system of conservation laws we will now consider the Euler equations of compressible gas dynamics.  These consist of conservation laws for *mass, momentum*, and *energy*.

### Mass Conservation

We will use $\rho(x,t)$ to denote the fluid density and $u(x,t)$ for its velocity.  Then the equation for conservation of mass is just the **continuity equation** we discussed in [Lesson 1](Lesson_01_Advection.ipynb):

$$\rho_t + (\rho u)_x = 0.$$

### Momentum Conservation

The momentum is given by the product of density and velocity, i.e. $\rho u$.  The momentum flux has two components.  First, the momentum is transported in the same way that the density is; this flux is given by the momentum times the density; i.e. $\rho u^2$.

To understand the second term in the momentum flux, we must realize that a fluid is made up of many tiny molecules.  The density and velocity we are modeling are average values over some small region of space.  The individual molecules in that region are not all moving with exactly velocity $u$; that's just their average.  Each molecule also has some additional random velocity component.  These random velocities are what accounts for the **pressure** of the fluid, which we'll denote by $p$.  These velocity components also lead to a net flux of momentum.  Thus the momentum conservation equation is

$$(\rho u)_t + (\rho u^2 + p)_x = 0.$$

### Energy Conservation

The energy has two components: internal energy $\rho e$ and kinetic energy $\rho u^2/2$:

$$E = \rho e + \frac{1}{2}\rho u^2.$$

Like the momentum flux, the energy flux involves both bulk transport ($Eu$) and transport due to pressure ($pu$):

$$E_t + (u(E+p)) = 0.$$

### Equation of State

You may have noticed that we have 4 unknowns (density, momentum, energy, and pressure) but only 3 conservation laws.  We need one more relation to close the system.  That relation, known as the equation of state, expresses how the pressure is related to the other quantities.  We'll focus on the case of an ideal gas, for which

$$p = \rho e (\gamma-1).$$

Here $\gamma$ is the ratio of specific heats, which for air is approximately 1.4.

### Full Equations

We can write the three conservation laws as a single system $q_t + f(q)_x = 0$ by defining
\begin{align}
q & = \begin{pmatrix} \rho \\ \rho u \\ E\end{pmatrix}, &
f(q) & = \begin{pmatrix} \rho u \\ \rho u^2 + p \\ u(E+p)\end{pmatrix}.
\end{align}

In three dimensions, the equations are similar.  We have two additional velocity components $v, w$, and their corresponding fluxes.  Additionally, we have to account for fluxes in the $y$ and $z$ directions.  We can write the full system as

$$ q_t + f(q)_x + g(q)_y + h(q)_z = 0$$

with

\begin{align}
q & = \begin{pmatrix} \rho \\ \rho u \\ \rho v \\ \rho w \\ E\end{pmatrix}, &
f(q) & = \begin{pmatrix} \rho u \\ \rho u^2 + p \\ \rho u v \\ \rho u w \\ u(E+p)\end{pmatrix} &
g(q) & = \begin{pmatrix} \rho v \\ \rho uv \\ \rho v^2 + p \\ \rho v w \\ v(E+p)\end{pmatrix} &
h(q) & = \begin{pmatrix} \rho w \\ \rho uw \\ \rho vw \\ \rho w^2 + p \\ w(E+p)\end{pmatrix}.
\end{align}

### Solving the Euler Equations

These equations can be solved in a manner similar to what we used for advection and traffic flow.  As you might guess, computing the flux gets significantly more complicated since we now have 3 (or 5) equations and more complicated flux expressions.

Implementing a solver for the Euler equations from scratch would be a lot of fun, but to save some time we'll use a  package called [PyClaw](http://clawpack.github.io/doc/pyclaw/), which is part of the [Clawpack](http://clawpack.github.io/) software (Clawpack stands for Conservation LAWs PACKage).  PyClaw allows us to quickly and easily set up and solve problems modeled by hyperbolic PDEs.

Now let's get started.  First, import the parts of Clawpack that we'll use:

```python
from clawpack import pyclaw
from clawpack import riemann
```

### Setting up a problem
To solve a problem, we'll need to create the following:

- A **domain** over which to solve the problem
- A **solution**, where we will provide the initial data.  After running, the solution will contain -- you guessed it! -- the solution.
- A **solver**, which is responsible for actually evolving the solution in time.  Here we'll need to specify the equations to be solved and the boundary conditions.
- A **controller**, which handles the running, output, and can be used for plotting

This might sound complicated at first, but stick with me.

Let's start by creating a controller and specifying the simulation end time:

```python
claw = pyclaw.Controller()
claw.tfinal = 0.1                    # Simulation end time

claw.keep_copy = True          # Keep solution data in memory for plotting
claw.output_format = None    # Don't write solution data to file
claw.num_output_times = 50  # Write 50 output frames
```

### Riemann solvers

The method used to compute the flux between each pair of cells is referred to as a *Riemann solver*.  By specifying a Riemann solver, we will specify the system of PDEs that we want to solve.  So far we have only used very simple approximate Riemann solvers.  Clawpack includes much more sophisticated Riemann solvers for many hyperbolic systems.

Place your cursor at the end of the line in the box below and hit 'Tab' (for autocompletion).  You'll see a dropdown list of all the Riemann solvers currently available in PyClaw.  The ones with 'py' at the end of the name are written in pure Python; the others are written in Fortran and wrapped with f2py.

```python
dir(riemann)
```

We'll start with a simple 1D problem, using the Riemann solver `riemann.euler_with_efix_1D`:

```python
riemann_solver = riemann.euler_with_efix_1D
claw.solver = pyclaw.ClawSolver1D(riemann_solver)
```

We also need to specify boundary conditions.  We'll use extrapolation boundary conditions, so that waves simply pass out of the domain:

```python
claw.solver.all_bcs = pyclaw.BC.extrap
```

### The problem domain
Next we need to specify the domain and the grid.  We'll solve on the unit line $[0,1]$ using 100 grid cells.  Note that each argument to the Domain constructor must be a tuple:

```python
domain = pyclaw.Domain( (0.,), (1.,), (100,))
```

### The initial solution
Next we create a solution object that belongs to the controller and extends over the domain we specified:

```python
claw.solution = pyclaw.Solution(claw.solver.num_eqn,domain)
```

### The initial solution
Next we create a solution object that belongs to the controller and extends over the domain we specified:

```python
claw.solution = pyclaw.Solution(3,domain)
```

The initial data is specified in an array named `solution.q`.  The density is contained in `q[0,:]`, the momentum in `q[1,:]`, and the energy in `q[2,:]`.

```python
x=domain.grid.x.centers # grid cell centers
gam = 1.4 # ratio of specific heats

rho_left = 1.0; rho_right = 0.125
p_left = 1.0; p_right = 0.1

claw.solution.q[0,:] = (x<0.5)*rho_left + (x>=0.5)*rho_right
claw.solution.q[1,:] = 0.
claw.solution.q[2,:] = ((x<0.5)*p_left + (x>=0.5)*p_right)/(gam-1.0)

fig = plt.figure()
axes = fig.add_subplot(1, 1, 1)
axes.plot(x, claw.solution.q[0,:],'-o')
axes.set_xlim((0.0, 1.0))
axes.set_ylim((-0.1, 1.1))
```

This problem is known as the *Sod shock-tube*.  It amounts to setting up a tube with a thin separator between a high-pressure, high-density region and a low-pressure, low-density region, then suddenly removing the separator.

Next we need to specify the value of $\gamma$, the ratio of specific heats.

```python
problem_data = claw.solution.problem_data
problem_data['gamma'] = 1.4
problem_data['gamma1'] = 0.4
```

Finally, let's run the simulation.

```python
claw.run()
```

### Plotting
Now we'll plot the results, which are contained in a list called `claw.frames`.  It's simple to plot a single frame with matplotlib:

```python
figs = []
for frame in claw.frames:
    figs.append(plt.figure())
    axes = figs[-1].add_subplot(1, 1, 1)
    axes.plot(x, frame.q[0, :], 'o-', linewidth=2)
    axes.set_xlim((0, 1))
    axes.set_ylim((-0.2, 1.2))
    plt.close(figs[-1])

# Animate the solution
images = animation.make_images(figs)
animation.JSAnimate_images(images)
```

### Waves

In the solution, 3 waves are visible:
1. A **shock wave** moving rapidly to the right as the low-density fluid is compressed.
2. A **rarefaction** wave moving to the left as the high-density fluid expands.
3. A **contact discontinuity** moving more slowly to the right.  This discontinuity in the density separates the region containing fluid that started in the high-pressure region and fluid that started in the low-pressure region.

In fact, the solution of any Riemann problem consists of some combination of these three types of waves.  In the Euler equations, one of the waves is always a contact discontinuity, but each of the other two waves may be a shock or a rarefaction, depending on the left and right states.

### Putting it all together

For convenience, all of the code from the cells above to set up and run the shocktube problem is pasted together below.  Play around with the code.  You might:
- Increase the number of grid points to see what the solution converges to.  Notice that the code still runs pretty fast even for larger grids.  This is because the bottom layer of code in PyClaw is compiled Fortran, not Python.
- Change the initial left and right states, or set up a completely different initial condition.  See if you can generate a solution with two shock waves, or two rarefaction waves (some physical intuition is helpful here).
- Change the ratio of specific heats
- Make the boundaries periodic, so that there is a second shock wave moving left from $x=1$.

```python
from clawpack import pyclaw
from clawpack import riemann

claw = pyclaw.Controller()
claw.tfinal = 0.2

claw.keep_copy = True       # Keep solution data in memory for plotting
claw.output_format = None   # Don't write solution data to file
claw.num_output_times = 50  # Write 50 output frames

riemann_solver = riemann.euler_with_efix_1D
claw.solver = pyclaw.ClawSolver1D(riemann_solver)

claw.solver.all_bcs = pyclaw.BC.extrap

domain = pyclaw.Domain( (0.,), (1.,), (100,))
x=domain.grid.x.centers # grid cell centers

claw.solution = pyclaw.Solution(claw.solver.num_eqn,domain)

gam = 1.4 # ratio of specific heats
claw.solution.problem_data['gamma'] = gam
claw.solution.problem_data['gamma1'] = gam-1.0

rho_left = 1.0; rho_right = 0.125
p_left = 1.0; p_right = 0.1

claw.solution.q[0,:] = (x<0.5)*rho_left + (x>=0.5)*rho_right
claw.solution.q[1,:] = 0.
claw.solution.q[2,:] = ((x<0.5)*p_left + (x>=0.5)*p_right)/(gam-1.0)

status = claw.run()

figs = []
for frame in claw.frames:
    figs.append(plt.figure())
    axes = figs[-1].add_subplot(1, 1, 1)
    axes.plot(x, frame.q[0, :], 'o-', linewidth=2)
    axes.set_xlim((0, 1))
    axes.set_ylim((-0.2, 1.2))
    plt.close(figs[-1])

# Animate the solution
images = animation.make_images(figs)
animation.JSAnimate_images(images)
```


# Variable Coefficient Poisson

Derive the form of a test variable-coefficient elliptic equation with periodic boundary conditions for testing the variable-coefficient multigrid solver.

We want to solve an equation of the form $\nabla \cdot (\alpha \nabla \phi) = f$

Note: it is important for solvability that the RHS, f, integrate to zero over our domain.  It seems sufficient to ensure that phi integrates to zero to have this condition met.

```python
%pylab inline
```

    Populating the interactive namespace from numpy and matplotlib

```python
from sympy import init_session
init_session()
```

    IPython console for SymPy 1.0 (Python 2.7.12-64-bit) (ground types: gmpy)

    These commands were executed:
    >>> from __future__ import division
    >>> from sympy import *
    >>> x, y, z, t = symbols('x y z t')
    >>> k, m, n = symbols('k m n', integer=True)
    >>> f, g, h = symbols('f g h', cls=Function)
    >>> init_printing()

    Documentation can be found at http://docs.sympy.org/1.0/

Variable coefficient elliptic problem, using periodic boundary conditions all around.

```python
alpha = 2.0 + cos(2*pi*x)*cos(2*pi*y)
```

```python
phi = sin(2*pi*x)*sin(2*pi*y)
```

we want to compute $\nabla \cdot (\alpha \nabla \phi)$

```python
phi_x = diff(phi, x)
phi_y = diff(phi, y)
```

```python
f = diff(alpha*phi_x, x) + diff(alpha*phi_y, y)
```

```python
f = simplify(f)
f
```

```python
print(f)
```

    -16.0*pi**2*(cos(2*pi*x)*cos(2*pi*y) + 1)*sin(2*pi*x)*sin(2*pi*y)

boundary conditions check

```python
phi.subs(x, 0)
```

```python
phi.subs(x, 1)
```

```python
phi.subs(y, 0)
```

```python
phi.subs(y, 1)
```

# General Elliptic

Derive the form of a test variable-coefficient elliptic equation with periodic boundary conditions for testing the variable-coefficient multigrid solver.

We are solving

$$\alpha \phi + \nabla \cdot (\beta \nabla \phi ) + \gamma \cdot \nabla \phi = f$$

Note: it is important for solvability that the RHS, f, integrate to zero over our domain.  It seems sufficient to ensure that phi integrates to zero to have this condition met.

```python
phi = sin(2*pi*x)*sin(2*pi*y)
```

```python
phi
```

```python
alpha = 1.0
beta = 2.0 + cos(2*pi*x)*cos(2*pi*y)
gamma_x = sin(2*pi*x)
gamma_y = sin(2*pi*y)

```

```python
alpha
```

```python
beta
```

```python
gamma_x
```

```python
gamma_y
```

```python
phi_x = diff(phi, x)
phi_y = diff(phi, y)
```

```python
f = alpha*phi + diff(beta*phi_x, x) + diff(beta*phi_y, y) + gamma_x*phi_x + gamma_y*phi_y
```

```python
f = simplify(f)
f
```

```python
print(f)
```

    (-16.0*pi**2*cos(2*pi*x)*cos(2*pi*y) + 2.0*pi*cos(2*pi*x) + 2.0*pi*cos(2*pi*y) - 16.0*pi**2 + 1.0)*sin(2*pi*x)*sin(2*pi*y)

```python
print(alpha)
```

    1.0

```python
print(beta)
```

    cos(2*pi*x)*cos(2*pi*y) + 2.0

```python
print(gamma_x)
```

    sin(2*pi*x)

```python
print (gamma_y)
```

    sin(2*pi*y)

```python

```

## Inhomogeneous BCs

```python
phi = cos(Rational(1,2)*pi*x)*cos(Rational(1,2)*pi*y)
```

```python
phi
```

```python
alpha = 10
gamma_x = 1
gamma_y = 1
beta = x*y + 1
```

```python
phi_x = diff(phi, x)
phi_y = diff(phi, y)
f = alpha*phi + diff(beta*phi_x, x) + diff(beta*phi_y, y) + gamma_x*phi_x + gamma_y*phi_y
```

```python
f
```

```python
simplify(f)
```

### boundary conditions

```python
phi.subs(x,0)
```

```python
phi.subs(x,1)
```

```python
phi.subs(y,0)
```

```python
phi.subs(y,1)
```

```python

```


# Taylor problem 2.43

last revised: 08-Jan-2019 by Dick Furnstahl [furnstahl.1@osu.edu]

**Go through and fill in the blanks where ### appears.**

The goal of this problem is to plot the trajectory of a basketball with specified parameters, with and without air resistance.  From the plot we are to find how far the basketball travels horizontally in the two cases before hitting the floor.

The differential equations to solve are equation (2.61) in Taylor:

$
\begin{align}
    m \ddot x &= - c \sqrt{\dot x^2 + \dot y^2}\, \dot x  \\
    m \ddot y &= - mg - c \sqrt{\dot x^2 + \dot y^2}\, \dot y
\end{align}
$

where the initial conditions we are given (in meters) are:

$
\begin{align}
   x(0) &= 0 \;, & y(0) &= 2  \\
   \dot x(0) &= 15 \cos \pi/4 \;, & \dot y(0) &= 15\sin \pi/4
\end{align}
$

and $c = \gamma D^2$ with $\gamma = 0.25$ and $D = 0.24\,$m.

### Gameplan:

We'll follow the treatment at the end of Taylor_problem_1.39.ipynb.  So we rewrite the coupled 2nd-order differential equations $x(t)$ and $y(t)$ as four coupled 1st-order differential equations for $x(t)$, $y(t)$, $v_x(t)$, and $v_y(t)$:

$
\begin{align}
   \frac{d}{dt}\left(\begin{array}{c}
                          \mathbf{x} \\
                          \mathbf{v}
                      \end{array}\right)
               = \left(\begin{array}{c}
                          \mathbf{v} \\
                          \mathbf{F}/m
                       \end{array}\right)
      \qquad \Longrightarrow \qquad
   \frac{d}{dt}\left(\begin{array}{c}
                          x \\
                          y \\
                          v_x \\
                          v_y
                      \end{array}\right)
               = \left(\begin{array}{c}
                          v_x \\
                          v_y \\
                          F_x/m \\
                          F_y/m
                       \end{array}\right)
\end{align}
$

```python
### Import what you need to make arrays, to make plots, and to use odeint.
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import odeint
from scipy.integrate import solve_ivp
```

```python
def ode_rhs(u_vec, t, *params):
    """
    Right-hand side (rhs) of the differential equation, with
    u_vec = [x, y, v_x, v_y] and params = [m, g, c].
    """
    x, y, v_x, v_y = u_vec
    m, g, c = params
    return [v_x, v_y
            , -c/m * np.sqrt(v_x**2 + v_y**2) * v_x
            ,-g -(c/m) * np.sqrt(v_x**2 + v_y**2) * v_y]
```

```python
m = 0.6      # units: kg
g = 9.8      # units: m/s^2
gamma = 0.25 # units: N s^2/m^4
D = 0.24     # units: m
c = gamma * D**2

theta = np.pi/4.  # angle of launch
v0 = 15.          # launch speed

# set the initial conditions
u0_vec = [0, 2, v0*np.cos(theta), v0*np.cos(theta) ]  ### complete the initial conditions

t_max = 3.  # integration time
t_pts = np.arange(-0.5, 3.)  ### create a suitable array of t values

# absolute and relative tolerances for ode solver
abserr = 1.0e-8
relerr = 1.0e-6

# Integrate the differential equation
#  Note the use of T here to take the *transpose* to change a
#  (# points) by 4 array to a 4 by (# points) array.
x, y, v_x, v_y = odeint(ode_rhs, u0_vec, t_pts, args=(m, g, c),
                        atol=abserr, rtol=relerr).T
```

Check the shape (i.e., dimensions) of the output from odeint:

```python
(odeint(ode_rhs, u0_vec, t_pts, args=(m, g, c),
                        atol=abserr, rtol=relerr)).shape
```

    (4, 4)

```python
(odeint(ode_rhs, u0_vec, t_pts, args=(m, g, c),
                        atol=abserr, rtol=relerr).T).shape
```

    (4, 4)

Define a function for the trajectory in vacuum, using the analytic formula:

```python
### Just check that this makes sense
def vacuum_trajectory(u0_vec, t_pts, g=9.8):
    """Given the initial condition vector and an array of time values,
       return the trajectory (x and y arrays) in the vacuum, using the
       exact solution.
    """
    x0, y0, v0_x, v0_y = u0_vec
    return v0_x * t_pts, \
           y0 + v0_y * t_pts - g * t_pts**2/2.

# create arrays for the vaccum trajectory (using default for g)
x_vac, y_vac = vacuum_trajectory(u0_vec, t_pts)
```

Make a plot with and without drag:

```python
fig = plt.figure(figsize=(6,4))
ax = fig.add_subplot(1,1,1)
ax.plot(x, y, 'b-', label='air resistance', )
ax.plot(x_vac, y_vac, 'r--', label='vacuum', )
ax.set_ylim(-0.5, 20.)    ### set appropriate limits on the y-axis
ax.set_xlim(0, 30.)    ### set appropriate limits on the x-axis
ax.set_xlabel('x (m)')
ax.set_ylabel('y (m)')

ax.legend()
### add a line to save this figure
```

We can read off rough values for the distances where the ball hits the ground.  Or we can find the closest values to zero in the arrays for `y` and `y_vac` and look up the value of `x` and `x_vac` at the same array index:

```python
# Note the use of fabs(y), which is |y|, to get closest to
#  zero instead of the most negative value of y.
index_y_min = (np.fabs(y)).argmin() # finds nearest index to minimum of |y|
print(f'distance with drag = {x[index_y_min]:.2f}' )

index_y_vac_min = (np.fabs(y_vac)).argmin() # finds nearest index to minimum of |y|
print(f'distance in vacuum = {x_vac[index_y_vac_min]:.2f}' )

```

    distance with drag = 0.00
    distance in vacuum = 26.52

**What change could you make to get a better estimate with the same method?**

```python

```


# Realization of Recursive Filters

*This jupyter notebook is part of a [collection of notebooks](../index.ipynb) on various topics of Digital Signal Processing. Please direct questions and suggestions to [Sascha.Spors@uni-rostock.de](mailto:Sascha.Spors@uni-rostock.de).*

## Introduction

Computing the output $y[k] = \mathcal{H} \{ x[k] \}$ of a [linear time-invariant](https://en.wikipedia.org/wiki/LTI_system_theory) (LTI) system is of central importance in digital signal processing. This is often referred to as [*filtering*](https://en.wikipedia.org/wiki/Digital_filter) of the input signal $x[k]$. We already have discussed the realization of [non-recursive filters](../nonrecursive_filters/introduction.ipynb). This section focuses on the realization of recursive filters.

### Recursive Filters

Linear difference equations with constant coefficients represent linear-time invariant (LTI) systems

\begin{equation}
\sum_{n=0}^{N} a_n \; y[k-n] = \sum_{m=0}^{M} b_m \; x[k-m]
\end{equation}

where $y[k] = \mathcal{H} \{ x[k] \}$ denotes the response of the system to the input signal $x[k]$, $N$ the order, $a_n$ and $b_m$ constant coefficients, respectively. Above equation can be rearranged with respect to the output signal $y[k]$ by extracting the first element ($n=0$) of the left hand sum

\begin{equation}
y[k] = \frac{1}{a_0} \left( \sum_{m=0}^{M} b_m \; x[k-m] - \sum_{n=1}^{N} a_n \; y[k-n] \right)
\end{equation}

It is evident that the output signal $y[k]$ at time instant $k$ is given as a linear combination of past output samples $y[k-n]$ superimposed by a linear combination of the actual $x[k]$ and past $x[k-m]$ input samples. Hence, the actual output $y[k]$ is composed from the two contributions

1. a [non-recursive part](../nonrecursive_filters/introduction.ipynb#Non-Recursive-Filters), and
2. a recursive part where a linear combination of past output samples is fed back.

The impulse response of the system is given as the response of the system to a Dirac impulse at the input $h[k] = \mathcal{H} \{ \delta[k] \}$. Using above result and the properties of the discrete Dirac impulse we get

\begin{equation}
h[k] = \frac{1}{a_0} \left( b_k  - \sum_{n=1}^{N} a_n \; h[k-n] \right)
\end{equation}

Due to the feedback, the impulse response will in general be of infinite length. The impulse response is termed as [infinite impulse response](https://en.wikipedia.org/wiki/Infinite_impulse_response) (IIR) and the system as recursive system/filter.

### Transfer Function

Applying a $z$-transform to the left and right hand side of the difference equation and rearranging terms yields the transfer function $H(z)$ of the system

\begin{equation}
H(z) = \frac{Y(z)}{X(z)} = \frac{\sum_{m=0}^{M} b_m \; z^{-m}}{\sum_{n=0}^{N} a_n \; z^{-n}}
\end{equation}

The transfer function is given as a [rational function](https://en.wikipedia.org/wiki/Rational_function) in $z$. The polynominals of the numerator and denominator can expressed alternatively by their roots as

\begin{equation}
H(z) = \frac{b_M}{a_N} \cdot \frac{\prod_{\mu=1}^{P} (z - z_{0\mu})^{m_\mu}}{\prod_{\nu=1}^{Q} (z - z_{\infty\nu})^{n_\nu}}
\end{equation}

where $z_{0\mu}$ and $z_{\infty\nu}$ denote the $\mu$-th zero and $\nu$-th pole of degree $m_\mu$ and $n_\nu$ of $H(z)$, respectively. The total number of zeros and poles is denoted by $P$ and $Q$. Due to the symmetries of the $z$-transform, the transfer function of a real-valued system $h[k] \in \mathbb{R}$ exhibits complex conjugate symmetry

\begin{equation}
H(z) = H^*(z^*)
\end{equation}

Poles and zeros are either real valued or conjugate complex pairs for real-valued systems ($b_m\in\mathbb{R}$, $a_n\in\mathbb{R}$). For the poles of a causal and stable system $H(z)$ the following condition has to hold

\begin{equation}
\max_{\nu} | z_{\infty\nu} | < 1
\end{equation}

Hence all poles have to be located inside the unit circle $|z| = 1$. Amongst others, this implies that $M \leq N$.

### Example

The following example shows the pole/zero diagram, the magnitude and phase response, and impulse response of a recursive filter with so called [Butterworth](https://en.wikipedia.org/wiki/Butterworth_filter) lowpass characteristic.

```python
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.markers import MarkerStyle
from matplotlib.patches import Circle
import scipy.signal as sig

N = 5  # order of recursive filter

def zplane(z, p):

    fig = plt.figure(figsize=(5,5))
    ax = fig.gca()
    plt.hold(True)

    unit_circle = Circle((0,0), radius=1, fill=False,
                         color='black', ls='solid', alpha=0.9)
    ax.add_patch(unit_circle)
    ax.axvline(0, color='0.7')
    ax.axhline(0, color='0.7')
    plt.axis('equal')
    plt.xlim((-2, 2))
    plt.ylim((-2, 2))
    plt.grid()
    plt.title('Poles and Zeros')
    plt.xlabel(r'Re{$z$}')
    plt.ylabel(r'Im{$z$}')

    ax.plot(np.real(z), np.imag(z), 'bo', fillstyle='none', ms = 10)
    ax.plot(np.real(p), np.imag(p), 'rx', fillstyle='none', ms = 10)

    plt.hold(False)

# coefficients of recursive filter
b, a = sig.butter(N, 0.2, 'low')
# compute transfer function of filter
Om, H = sig.freqz(b, a)
# compute impulse response
k = np.arange(128)
x = np.where(k==0, 1.0, 0)
h = sig.lfilter(b, a, x)

# plot pole/zero-diagram
zplane(np.roots(b), np.roots(a))
# plot magnitude response
plt.figure(figsize=(10, 3))
plt.plot(Om, 20 * np.log10(abs(H)))
plt.xlabel(r'$\Omega$')
plt.ylabel(r'$|H(e^{j \Omega})|$ in dB')
plt.grid()
plt.title('Magnitude response')
# plot phase response
plt.figure(figsize=(10, 3))
plt.plot(Om, np.unwrap(np.angle(H)))
plt.xlabel(r'$\Omega$')
plt.ylabel(r'$\varphi (\Omega)$ in rad')
plt.grid()
plt.title('Phase')
# plot impulse response
plt.figure(figsize=(10, 3))
plt.stem(20*np.log10(np.abs(np.squeeze(h))))
plt.xlabel(r'$k$')
plt.ylabel(r'$|h[k]|$ in dB')
plt.grid()
plt.title('Impulse response');
```

**Exercise**

* Does the system have an IIR?
* What happens if you increase the order `N` of the filter?

**Copyright**

This notebook is provided as [Open Educational Resource](https://en.wikipedia.org/wiki/Open_educational_resources). Feel free to use the notebook for your own purposes. The text is licensed under [Creative Commons Attribution 4.0](https://creativecommons.org/licenses/by/4.0/), the code of the IPython examples under the [MIT license](https://opensource.org/licenses/MIT). Please attribute the work as follows: *Sascha Spors, Digital Signal Processing - Lecture notes featuring computational examples, 2016-2017*.


# Mathematical Modeling Project 3
## Semiconductor Wafer Production Queuing Model
### By: James Tourkistas

```python
import numpy as np
import matplotlib.pyplot as plt
import random
import pandas as pd
```

### Poisson Draws for Daily Wafer Orders
**$\begin{align}P\left( k \right) = \frac{{e^{ - \lambda } \lambda ^k }}{{x!}}\end{align}$**

$\begin{align}Parameter:\end{align}$
$\begin{align}\lambda=u*\frac{AnnualProductionCapacity}{DaysInYear}\end{align}$

```python
# copy custom Pvar function from pvar.ipynb
###Single Poisson Draws. Pick lambda
def Pvar(mu):
    '''
    Args:
        mu :    mean of Poisson random variate

    Returns:
        Poisson random variate with mean mu
    '''

    r = np.random.uniform()    # number from uniform distribution [0,1)
    p = np.exp(-mu)            # Poisson probability
    ptot = p                   # will accumulate the probability variate <= k
    k = 0                      # trial variate

    while r > ptot:
        k += 1
        p = p * mu / k         # probability of k + 1 using the recursive relation
        ptot += p

    return k
```

#### Note: Formulation of poisson draws from Mathematical Modeling class.

### Multinomial Draws (Order Type Breakdown)

#### Demand Probabilities by Producible Chip Size:
#### (See Calculations Below)
**5nm:**.02

**7nm:**.17

**10nm:**.002

**16nm:**.20

**20nm:**.009

**28nm**:.21

**40/45nm**:.18

**65nm:**.13

**90nm:**.07

```python
####Stochastic Daily Demand Draws

def multinomial_order_type(n):

    '''
    Args:
        n :    number of orders for period

    returns breakdown by order type
    '''
    order_prob=np.array([0.02170595, 0.17131066, 0.00230304, 0.20148758, 0.00931966,
       0.20950821, 0.18398519, 0.12817502, 0.0722047 ])  ###See Calculation Below
    orders_dict={"5nm":0,"7nm":0,"10nm":0,"16nm":0,"20nm":0,"28nm":0,"40/45nm":0,"65nm":0,"90nm":0}
    daily_orders=[]

    for i in range(n):
        unif=np.random.rand(1)
        if unif<np.sum(order_prob[:1]):
            daily_orders.append("5nm")
        elif unif<np.sum(order_prob[:2]):
            daily_orders.append("7nm")
        elif unif<np.sum(order_prob[:3]):
            daily_orders.append("10nm")
        elif unif<np.sum(order_prob[:4]):
            daily_orders.append("16nm")
        elif unif<np.sum(order_prob[:5]):
            daily_orders.append("20nm")
        elif unif<np.sum(order_prob[:6]):
            daily_orders.append("28nm")
        elif unif<np.sum(order_prob[:7]):
            daily_orders.append("40/45nm")
        elif unif<np.sum(order_prob[:8]):
            daily_orders.append("65nm")
        else:
            daily_orders.append("90nm")

    ###Add Random Draws to Dictionary d
    d = {x:daily_orders.count(x) for x in daily_orders}

    ###Add 0 to dictionary for chips without orders
    for k in d.keys():
        orders_dict[k]=d[k]

    return orders_dict
#pd.DataFrame(pd.DataFrame(demand_vec)[0].value_counts()).reset_index()

```

### Set Parameters:

**$\begin{align}tmax:\end{align}$** days to run model.

**$\begin{align}qmax:\end{align}$** vector representing maximum queued orders allowed for machine.

**$\begin{align}Wafers Per Day:\end{align}$** vector representing average # of wafers produced per day by each machine type.

**$\begin{align}nmachines:\end{align}$** number of different wafer type machines

 $\begin{align}u:\end{align}$ fab capacity utilization parameter between 0 and 1 depending on average plant capacity used.

```python
###Initial Variables
# Set Up Parameters for Model.
nmachines = 9  ###Machines Types
tmax=10  ###Days Model Run For
col_nams=["5nm","7nm","10nm","16nm","20nm","28nm","40/45nm","65nm","90nm"]   ###Types of Wafers Produced
qmax=np.array([2,3,2,2,100,100,100,100,100]) ###Maximum orders for each machine type
wafers_per_day=np.array([5,10,15,30,50,70,60,100,500]) ##Maximum daily processing capacity for each machine

```

### Multiple Machine Capacity Queuing Model:

```python
###Initialize Variables to 0
total_orders=0
idle=0
idle_list=[]
lost=0
wait=0
wait_list=[]
t=0
q=[] #an empty queue
lost_orders=[]
machines=np.zeros((nmachines,1)) # all machines free to start
q = [[] for _ in range(nmachines)]# each machine's queue starts empty,
df = pd.DataFrame(columns=col_nams)
u=1
###RUN MODEL

while t < tmax:

    t=t+1 # first, let time pass

    new_orders=Pvar(u*(120000/365))  ##New Orders drawn from Poisson Distribution

    total_orders=total_orders+new_orders  ###Order Tally

    orders_dict=multinomial_order_type(new_orders) ###Order Breakdown from Multinomial Distribution

    df = df.append(orders_dict, ignore_index = True)  ###Append New Orders To Table

    ###Create an array with order counts

    wafer_breakdown=np.array([orders_dict["5nm"],orders_dict["7nm"],orders_dict["10nm"],orders_dict["16nm"],orders_dict["20nm"],orders_dict["28nm"],orders_dict["40/45nm"],orders_dict["65nm"],orders_dict["90nm"]])

    ####ADD NEW ORDERS TO MACHINE QUEUE

    for k in range(nmachines):

        if len(q[k]) < qmax[k]: # Check Machine Queue.
            q[k].append([t,wafer_breakdown[k]]) # [time of arrival, time to complete] Service time follows a shifted Poisson distribution

        ###REFUSE ORDERS EXCEEDING MAXIMUM CAPACITY for MACHINE

        else:
            lost_orders.append(col_nams[k])
            lost_orders.append(wafer_breakdown[k])
            lost=lost+1

    ###ROUTE ORDERS TO MACHINE AND COMPLETE ORDERS

    for i in range(nmachines):

        if machines[i] >= wafers_per_day[i]:
            machines[i]-=wafers_per_day[i] #If queued order is above wafer per day capacity, reduce by capacity

        elif machines[i]< wafers_per_day[i]:

            qlength = len(q[i])

            if qlength > 0:

                ###REMOVE ORDER(S) FROM QUEUE TO UTILIZE EXCESS MACHINE CAPACITY
                wt=t-q[i][0][0] ###Calculate Wait Time From Earliest Order.
                wait_list.append(col_nams[i])
                wait_list.append(wt)
                wait +=wt
                b=qlength*2+1
                extra_capacity=wafers_per_day[i]-machines[i]
                arr=np.hstack(q[i])[1:b:2]   ####Retrieve Only Order Values
                index=np.sum(np.cumsum(arr)<=extra_capacity) ####Find Index Greater Than Given Capacity
                ind_plus=index+1
                machines[i]=np.sum(arr[0:ind_plus])-extra_capacity ###Add Incomplete Portion of Order to Machine

                ##Set negative to 0
                if machines[i]<0:
                    idlt=1-((-machines[i])/wafers_per_day[i]) ##Calculate idle time
                    idle_list.append(col_nams[i])
                    idle_list.append(idlt[0])
                    idle=idle+idlt ###Add Idle Time To Idle
                    machines[i]=0 ####Set q to 0 if extra capacity exceeds orders in queue

                q[i]=q[i][ind_plus:][:] ###Remove orders from queue

            else:
                q[i] = []  #IF ALL QUEUED JOBS COMPLETE, SET QUEUE TO NO ORDER STATUS
                idlt=1-(machines[i]/wafers_per_day[i]) ##Calculate idle time
                idle_list.append(col_nams[i])
                idle_list.append(idlt[0])
                idle=idle+idlt  ###Add Idle Time To Idle
                machines[i]=0

##### AT END OF TIME COUNT LOST JOBS
for i in range(nmachines):
    lost += len(q[i]);  # Orders Remaining At End of Period Are Lost
    m=len(q[i])*2+1
    if m>1:
        lost_orders.append(col_nams[i])
        lost_orders.append(np.sum(np.hstack(q[i])[1:m:2])) ###Add Left Over Orders to Lost Orders

wait = wait/(total_orders-lost);  # average wait of those who joined the queue

print('Total Orders:', total_orders)
print('Wait Time:', wait)
print('Machine Idle Time:', idle[0])
print('Incomplete Orders:', lost)

###Calculate Completed and Missed Orders for Each Category
if len(lost_orders)>0:
    lost_orders_df=pd.DataFrame(columns=["lost_order_quantity","type"])
    lost_orders_df["type"]=lost_orders[0::2]
    lost_orders_df["lost_order_quantity"]=lost_orders[1::2]
    lo=lost_orders_df.groupby("type")["lost_order_quantity"].sum()

```

    Total Orders: 3279
    Wait Time: 0.004595588235294118
    Machine Idle Time: 13.785047619047615
    Incomplete Orders: 15

**Note:** Each order includes multiple wafers of the same type.

### Idle Time  Breakdown by Machine

```python
###Calculate Idle Machine Time for Each Category

idletime_df=pd.DataFrame(columns=["idle_time","type"])
idletime_df["type"]=idle_list[0::2]
idletime_df["idle_time"]=idle_list[1::2]
it=idletime_df.groupby("type")["idle_time"].sum()
```

### Total Wait Time  by Machine

```python
waittime_df=pd.DataFrame(columns=["wait_time","type"])
waittime_df["type"]=wait_list[0::2]
waittime_df["wait_time"]=wait_list[1::2]
wt2=waittime_df.groupby("type")["wait_time"].sum()
```

### Final Wafer Breakdown: Completed vs Missed Orders

```python
order_stats=pd.DataFrame(df.sum(axis=0))
order_stats=order_stats.join(pd.DataFrame(lo)).fillna(0)
order_stats=order_stats.join(pd.DataFrame(it)).fillna(0)
order_stats=order_stats.join(pd.DataFrame(wt2)).fillna(0)
order_stats.columns = ['Possible Wafer Orders', 'Missed Orders',"Idle Time","Total Wait"]
order_stats["Completed Wafers"]=order_stats['Possible Wafer Orders']-order_stats['Missed Orders']
Unit_Price_Breakdown=np.array([16988,9346,5992,3984,3677,2891,2274,1937,1650])
#See Kahn at 44.
order_stats["Est. Sale Price ($)"]=Unit_Price_Breakdown
order_stats["Period Revenue ($)"]=order_stats["Est. Sale Price ($)"]*order_stats["Completed Wafers"]

order_stats
```

      Possible Wafer Orders
      Missed Orders
      Idle Time
      Total Wait
      Completed Wafers
      Est. Sale Price ($)
      Period Revenue ($)

      5nm
      66.0
      14.0
      0.800000
      3
      52.0
      16988
      883376.0

      7nm
      575.0
      467.0
      0.000000
      3
      108.0
      9346
      1009368.0

      10nm
      16.0
      0.0
      1.066667
      0
      16.0
      5992
      95872.0

      16nm
      661.0
      299.0
      0.000000
      9
      362.0
      3984
      1442208.0

      20nm
      42.0
      0.0
      0.840000
      0
      42.0
      3677
      154434.0

      28nm
      694.0
      0.0
      2.885714
      0
      694.0
      2891
      2006354.0

      40/45nm
      569.0
      0.0
      3.416667
      0
      569.0
      2274
      1293906.0

      65nm
      433.0
      0.0
      4.330000
      0
      433.0
      1937
      838721.0

      90nm
      223.0
      0.0
      0.446000
      0
      223.0
      1650
      367950.0

```python
order_stats2=order_stats.reset_index()
```

### Plots

```python
plt.figure(figsize=(22,6))
plt.subplot(1, 3, 1)
plt.title('Period Revenue', fontweight="bold")

plt.bar(order_stats2["index"],order_stats2["Period Revenue ($)"])

plt.xlabel('Wafer Type')
plt.ylabel('Predicted Revenue ($)')

plt.subplot(1, 3, 2)
plt.title('Quantity Produced', fontweight="bold")

plt.bar(order_stats2["index"],order_stats2["Completed Wafers"])
plt.xlabel('Wafer Type')
plt.ylabel('Predicted Quantity Produced')

plt.subplot(1, 3, 3)
plt.title('Missed Orders', fontweight="bold")

plt.bar(order_stats2["index"],order_stats2['Missed Orders'])
plt.xlabel('Wafer Type')
plt.ylabel('Missed Orders')

```

```python
plt.figure(figsize=(14,4))
plt.subplot(1, 2, 1)
plt.title('Wait Time', fontweight="bold")

plt.bar(order_stats2["index"],order_stats2["Total Wait"])

plt.xlabel('Wafer Type')
plt.ylabel('Summed Wait Time')

plt.subplot(1, 2, 2)
plt.title('Idle Time', fontweight="bold")

plt.bar(order_stats2["index"],order_stats2["Idle Time"])
plt.xlabel('Wafer Type')
plt.ylabel('Summed Idle Time')
```

```python
print(f"Estimated Revenue from Sales Over Period ({tmax} days):$", sum(order_stats["Period Revenue ($)"]))
```

    Estimated Revenue from Sales Over Period (10 days):$ 8092189.0

### Calculations: Demand Probability by Chip Type for Multinomial Draws
The model draws its inspiration from Taiwan Semiconductor Manufacturing Company (TSMC). TSMC currently runs "four 12-inch wafer GIGAFAB® fabs, four 8-inch wafer fabs, one 6-inch wafer fab, one 12-inch wafer fab, and two 8-inch wafer fabs. They plan to build a new 5nm fab in Arizona with 20,000 semiconductor wafers per month. TSMC's total wafer production capacity is 12,000,0000 12 inch equivilant wafers. TSMC 2020 Annual Report. Since they are such a large wafer company, we assumed their sales would be representative of demand.

```python
# TSMC Revenue Breakdown: From TSCM Quarterly Presentation Q1 2021

#5nm,7nm,10nm,16nm,20nm,28nm,40/45nm,65nm,90nm,.11/.13um, .15/.18um,.25um+
##Revenue Percents
Q1_2021=np.array([.14,.35,0,.14,0,.11,.07,.05,.03,.03,.06,.02])
###Wafer Shipment
Q4_2020=np.array([.20,.29,0,.13,.01,.11,.08,.05,.02,.03,.07,.01])
Q3_2020=np.array([.14,.35,0,.14,0,.11,.07,.05,.03,.03,.06,.02])
Q1_2021

```

    array([0.14, 0.35, 0.  , 0.14, 0.  , 0.11, 0.07, 0.05, 0.03, 0.03, 0.06,
           0.02])

```python
###2020 Revenue Breakdown

#5nm,7nm,10nm,16nm,20nm,28nm,40/45nm,65nm,90nm, .11/.13um, .15/.18um,.25um+
all_rev_breakdown=np.array([90934485,394836964,3403151,197959003,8450865,149367729,103176542,61226671,29380358,33197137,86008475,20514893])

###5nm,7nm,10nm,16nm,20nm,28nm,40/45nm,65nm,90nm
NM_Rev_Breakdown=np.array([90934485,394836964,3403151,197959003,8450865,149367729,103176542,61226671,29380358])

#See TSMC 2020 Annual Report at 52.

Unit_Price_Breakdown=np.array([16988,9346,5992,3984,3677,2891,2274,1937,1650])

#See Kahn at 44.
```

#### TSMC did not report category price so we divided reported revenue by projected prices from Kahn paper.

```python
###5nm,7nm,10nm,16nm,20nm,28nm,40/45nm,65nm,90nm
Q_Breakdown=NM_Rev_Breakdown/Unit_Price_Breakdown
Q_Breakdown
```

    array([ 5352.86584648, 42246.62572223,   567.9490988 , 49688.50476908,
            2298.30432418, 51666.45762712, 45372.2700088 , 31609.01961797,
           17806.27757576])

#### Probabilities Used for Multinomial Model

$\begin{align}p_i=\frac{\frac{c_i}{\pi_i}}{\sum_{i=1}^{i}(\frac{c_i}{\pi_i})}\end{align}$

**c:** annual chip type revenue

**π:** average chip type price

```python
###5nm,7nm,10nm,16nm,20nm,28nm,40/45nm,65nm,90nm
#Normalized
Q_Probability=Q_Breakdown/np.sum(Q_Breakdown)
Q_Probability
```

    array([0.02170595, 0.17131066, 0.00230304, 0.20148758, 0.00931966,
           0.20950821, 0.18398519, 0.12817502, 0.0722047 ])

**References:**

1) McClean, E. 2018. Advanced Technology Key to Strong Foundry Revenue per Wafer. [online] IC Insights. Available at: <https://www.icinsights.com/data/articles/documents/1108.pdf> [Accessed 4 May 2021].

2) Kahn, S et al. 2020. AI Chips: What They Are and Why They Matter. [online] Georgetown Center for Security and Emerging Technology. Available at: <https://cset.georgetown.edu/research/ai-chips-what-they-are-and-why-they-matter> [Accessed 4 May 2021]. https://doi.org/10.51593/20190014

3) McClean, E. 2018. Revenue per Wafer Climbs As Demand Surges for 5nm/7nm IC Processes. [online] IC Insights. Available at: <https://www.icinsights.com/data/articles/documents/1350.pdf> [Accessed 4 May 2021].

4) Zafar, R. 2019. Apple A13 & Beyond: How Transistor Count And Costs Will Go Up. [online] wccftech. Available at: <https://wccftech.com/apple-5nm-3nm-cost-transistors/> [Accessed 4 May 2021].

5) Su, Jeff. April 2021. TSMC Quarterly Management Report. [online] TSMC. Available at: <https://investor.tsmc.com/english/encrypt/files/encrypt_file/reports/2021-04/c3503e3be4f98cd83817a8abff6e4586deaa3904/1Q21ManagementReport.pdf> [Accessed 5 May 2021].

6) TSMC. 2020. TSMC Annual Report. [online] TSMC. Available at: <ttps://investor.tsmc.com/sites/ir/annual-report/2020/2020Annual%20Report_E_%20.pdf> [Accessed 5 May 2021].

#### Note: Code was created by adapting Math Modeling module multiserver queueing materials.

## Deterministic Model (Still Under Construction):

```python
from scipy.integrate import solve_ivp
from scipy.integrate import odeint
from collections import Counter
```

```python
###TSMC Price Per 300mm Wafer by Feature Size

nm5=16746
nm7=9213
nm10=5907
nm12_16=3927
###Initial Inventory Capacity
u=1  ####Utilization
c=120000  #approximate quantity sold 30000 per month.
##Other Parameters

K_hat=-5400000000 ###Capital Investment
L=3000*164000  ### Annual Labor (Employees*Avg Salary)

K=(1000000000-L)  #### Non-Labor Operating Expenses
I_0=K_hat  #### Initial Profits
Q_0=u*c
R_0=0
###Initial Inventory Capacity
p=nm5 #2019 sales for TSMC foundary
r=q*p

#u=np.random.uniform(.5,1,6)
```

```python
###Semiconductor Model
t=np.linspace(0,5,6)
x_0=[Q_0,R_0,I_0]
def SM(x,t):
    Q,R,I = x
    dxSM= np.zeros(3)
    dxSM[0]=0 #Q
    dxSM[1]=Q*p-R #R
    dxSM[2]=Q*p-L-K #I(ROI)
    return dxSM

```

```python
####ODE INT METHOD (allows for multidimensional matrix)
sol_SM=odeint(SM,x_0,t)
t

```

    array([0., 1., 2., 3., 4., 5.])

```python
###Get Model Predictions
Q_Pred=sol_SM[:,0]
R_Pred=sol_SM[:,1]
I_Pred=sol_SM[:,2]
```

```python
plt.figure(figsize=(8,8))
plt.figure(figsize=(16,6))
plt.subplot(1, 3, 1)
plt.title('Q', fontweight="bold")

plt.plot(t,Q_Pred, color="black", label='Semiconductor Wafer Quantity')

plt.xlabel('Years')
plt.ylabel('Q')
plt.legend()

plt.subplot(1, 3, 2)
plt.title('I', fontweight="bold")

plt.plot(t,I_Pred, color="green", label='ROI')
plt.axhline(y=0, color="red", linestyle='--')
plt.xlabel('Years')
plt.ylabel('ROI')
plt.legend()

plt.subplot(1, 3, 3)
plt.title('R', fontweight="bold")

plt.plot(t,R_Pred, color="blue", label='Revenue')

plt.xlabel('Years')
plt.ylabel('Total Revenue')
plt.legend()

####Note: It takes a year to get up to full capacity.
```

```python

```

```python

```

Project 3 Semiconductor Wafer Machine Queue Model Code (Final Model) .ipynb


This notebooks shows how to derive transfer functions for analysis of longitudinal aircraft dynamics.

# Translational Equations of Motion

$\frac{^ed}{dt} m V = F$

Mechanized in inertial frame:
$\frac{^ed}{dt} m V_e = F_e$

$\frac{^ed}{dt} m V_e = F_e$

If m is constants, back to F= mA

$m \dot{V_e} = F_e$

$m A_e = F_e$

Mechanized in body frame:
$\frac{^bd}{dt} m V_b + \omega^{eb}_b \times (m V_b) = F_b$

If taking derivative of a vector with respect to frame it is expressed in,
it becomes the vector dot, time derivative of components.

$m \dot{V_b} + \omega^{eb}_b \times (m V_b) = F_b$

# Rotational Equations of Motion

$\frac{^ed}{dt} J \omega^{eb} = M$

$\frac{^bd}{dt} J \omega^{eb}_b + \omega^{eb}_b \times J \omega^{eb}_b  = M_b$

If J is constant (rotational inertial in body frame about cm):

$J \dot{\omega^{eb}_b} + \omega^{eb}_b \times J \omega^{eb}_b  = M_b$

```python
import sympy
import sympy.physics.mechanics as me
import scipy.optimize
import scipy.integrate
import matplotlib.pyplot as plt
import control
import numpy as np

m, J_y, g, t = sympy.symbols('m, J_y, g, t')
gamma, alpha, V, Q = me.dynamicsymbols('gamma, alpha, V, Q') # functions of time
frame_e = me.ReferenceFrame('e')
frame_w = frame_e.orientnew('w', 'Axis', (gamma, frame_e.y))
frame_b = frame_w.orientnew('b', 'Axis', (-alpha, frame_w.y))
J = me.inertia(frame_b, 0, J_y, 0)

# control inputs
th, elv = sympy.symbols('th, elv')  # thrust, elevator

T = th

# gravitational force
W = m*g
```

## Aerodynamics Model

```python
C_La, C_L0, k_CLCD, C_D0, C_M0, C_Me, C_MQ, C_Ma, rho, S, alpha_s = \
    sympy.symbols('C_La, C_L0, k_CLCD, C_D0, C_M0, C_Me, C_MQ, C_Ma, rho, S, alpha_s')

# aero coefficients, neglecting stall
C_L = C_L0 + C_La*alpha
C_D = C_D0 + k_CLCD*C_L**2
C_M = C_M0 + C_Ma*alpha + C_Me*elv + C_MQ*Q

# aero forces/moments
q = rho*V**2/2
D = C_D*q*S
L = C_L*q*S
M = C_M*q*S
```

```python
C_L
```

$\displaystyle C_{L0} + C_{La} \alpha{\left(t \right)}$

```python
alpha_var = sympy.symbols('alpha')
sympy.plot(C_L.subs({C_L0: 0.1, C_La: 3, alpha: alpha_var}),
           (alpha_var, -0.5, 0.4),
           xlabel='alpha',
           ylabel='CL')
sympy.plot(C_D.subs({C_D0: 0.02, k_CLCD: 0.1, C_L0: 0.1, C_La: 3, alpha: alpha_var}),
           (alpha_var, -0.5, 0.4),
           xlabel='alpha',
           ylabel='CL')
```

# Dynamics

Sum of forces.

```python
F_b = (-L*frame_w.z - D*frame_w.x + W*frame_e.z + T*frame_b.x).express(frame_b).simplify()
F_b
```

$\displaystyle (- \frac{S \rho \left(C_{D0} + k_{CLCD} \left(C_{L0} + C_{La} \alpha{\left(t \right)}\right)^{2}\right) V^{2}{\left(t \right)} \cos{\left(\alpha{\left(t \right)} \right)}}{2} - \frac{S \rho \left(C_{L0} + C_{La} \alpha{\left(t \right)}\right) V^{2}{\left(t \right)} \sin{\left(\alpha{\left(t \right)} \right)}}{2} + g m \sin{\left(\alpha{\left(t \right)} - \gamma{\left(t \right)} \right)} + th)\mathbf{\hat{b}_x} + (\frac{S \rho \left(C_{D0} + k_{CLCD} \left(C_{L0} + C_{La} \alpha{\left(t \right)}\right)^{2}\right) V^{2}{\left(t \right)} \sin{\left(\alpha{\left(t \right)} \right)}}{2} - \frac{S \rho \left(C_{L0} + C_{La} \alpha{\left(t \right)}\right) V^{2}{\left(t \right)} \cos{\left(\alpha{\left(t \right)} \right)}}{2} + g m \cos{\left(\alpha{\left(t \right)} - \gamma{\left(t \right)} \right)})\mathbf{\hat{b}_z}$

```python
F_w = (-L*frame_w.z - D*frame_w.x + W*frame_e.z + T*frame_b.x).express(frame_w).simplify()
F_w
```

$\displaystyle (- \frac{S \rho \left(C_{D0} + k_{CLCD} \left(C_{L0} + C_{La} \alpha{\left(t \right)}\right)^{2}\right) V^{2}{\left(t \right)}}{2} - g m \sin{\left(\gamma{\left(t \right)} \right)} + th \cos{\left(\alpha{\left(t \right)} \right)})\mathbf{\hat{w}_x} + (- \frac{S \rho \left(C_{L0} + C_{La} \alpha{\left(t \right)}\right) V^{2}{\left(t \right)}}{2} + g m \cos{\left(\gamma{\left(t \right)} \right)} + th \sin{\left(\alpha{\left(t \right)} \right)})\mathbf{\hat{w}_z}$

Find linear momentum take derivative in earth frame (Newton-Euler EOMs)

```python
LM = m*V*frame_w.x
eom_trans = LM.diff(t, frame_e).simplify() - F_w
eom_trans = eom_trans.to_matrix(frame_w)
eom_trans
```

$\displaystyle \left[\begin{matrix}\frac{S \rho \left(C_{D0} + k_{CLCD} \left(C_{L0} + C_{La} \alpha{\left(t \right)}\right)^{2}\right) V^{2}{\left(t \right)}}{2} + g m \sin{\left(\gamma{\left(t \right)} \right)} + m \frac{d}{d t} V{\left(t \right)} - th \cos{\left(\alpha{\left(t \right)} \right)}\\0\\\frac{S \rho \left(C_{L0} + C_{La} \alpha{\left(t \right)}\right) V^{2}{\left(t \right)}}{2} - g m \cos{\left(\gamma{\left(t \right)} \right)} - m V{\left(t \right)} \frac{d}{d t} \gamma{\left(t \right)} - th \sin{\left(\alpha{\left(t \right)} \right)}\end{matrix}\right]$

Sum of moments.

```python
M_b = (M*frame_b.y).express(frame_w).simplify()
M_b
```

$\displaystyle \frac{S \rho \left(C_{M0} + C_{MQ} Q{\left(t \right)} + C_{Ma} \alpha{\left(t \right)} + C_{Me} elv\right) V^{2}{\left(t \right)}}{2}\mathbf{\hat{w}_y}$

Finding angular momentum, take derivative in earth frame (Newton-Euler EOms)

```python
AM = J_y*frame_b.ang_vel_in(frame_e).express(frame_w).subs(gamma.diff(t) - alpha.diff(t), Q)
AM
```

$\displaystyle J_{y} Q{\left(t \right)}\mathbf{\hat{w}_y}$

# Equations of Motion

```python
eom_rot = (AM.diff(t, frame_e) - M_b).to_matrix(frame_b)
eom_rot
```

$\displaystyle \left[\begin{matrix}0\\J_{y} \frac{d}{d t} Q{\left(t \right)} - \frac{S \rho \left(C_{M0} + C_{MQ} Q{\left(t \right)} + C_{Ma} \alpha{\left(t \right)} + C_{Me} elv\right) V^{2}{\left(t \right)}}{2}\\0\end{matrix}\right]$

Create first order ODE model.

$\dot{x} = f(x, u, p)$

x: states
u: inputs
p: parameters

$\gamma = \theta - \alpha$

$\dot{\gamma} = \dot{\theta} - \dot{\alpha}$

$\dot{\theta} \equiv Q$

$Q = \dot{\gamma} + \dot{\alpha}$

```python
eoms = sympy.Matrix.vstack(eom_trans, eom_rot)
eoms = sympy.Matrix([
    eom_rot[1],
    eom_trans[0],
    eom_trans[2],
    gamma.diff(t) + alpha.diff(t) - Q
])
eoms
```

$\displaystyle \left[\begin{matrix}J_{y} \frac{d}{d t} Q{\left(t \right)} - \frac{S \rho \left(C_{M0} + C_{MQ} Q{\left(t \right)} + C_{Ma} \alpha{\left(t \right)} + C_{Me} elv\right) V^{2}{\left(t \right)}}{2}\\\frac{S \rho \left(C_{D0} + k_{CLCD} \left(C_{L0} + C_{La} \alpha{\left(t \right)}\right)^{2}\right) V^{2}{\left(t \right)}}{2} + g m \sin{\left(\gamma{\left(t \right)} \right)} + m \frac{d}{d t} V{\left(t \right)} - th \cos{\left(\alpha{\left(t \right)} \right)}\\\frac{S \rho \left(C_{L0} + C_{La} \alpha{\left(t \right)}\right) V^{2}{\left(t \right)}}{2} - g m \cos{\left(\gamma{\left(t \right)} \right)} - m V{\left(t \right)} \frac{d}{d t} \gamma{\left(t \right)} - th \sin{\left(\alpha{\left(t \right)} \right)}\\- Q{\left(t \right)} + \frac{d}{d t} \alpha{\left(t \right)} + \frac{d}{d t} \gamma{\left(t \right)}\end{matrix}\right]$

```python
x = sympy.Matrix([V, alpha, gamma, Q])
x.T
```

$\displaystyle \left[\begin{matrix}V{\left(t \right)} & \alpha{\left(t \right)} & \gamma{\left(t \right)} & Q{\left(t \right)}\end{matrix}\right]$

```python
u = sympy.Matrix([th, elv])
u.T
```

$\displaystyle \left[\begin{matrix}th & elv\end{matrix}\right]$

```python
h_dot = V*sympy.sin(gamma)
y = sympy.Matrix([V, alpha, gamma, Q, h_dot])
```

```python
dx = x.diff(t)
dx.T
```

$\displaystyle \left[\begin{matrix}\frac{d}{d t} V{\left(t \right)} & \frac{d}{d t} \alpha{\left(t \right)} & \frac{d}{d t} \gamma{\left(t \right)} & \frac{d}{d t} Q{\left(t \right)}\end{matrix}\right]$

```python
sol = sympy.solve(eoms, dx)
f = sympy.Matrix([sol[dxi] for dxi in dx])
f.simplify()
f
```

$\displaystyle \left[\begin{matrix}\frac{- \frac{C_{D0} S \rho V^{2}{\left(t \right)}}{2} - \frac{C_{L0}^{2} S k_{CLCD} \rho V^{2}{\left(t \right)}}{2} - C_{L0} C_{La} S k_{CLCD} \rho V^{2}{\left(t \right)} \alpha{\left(t \right)} - \frac{C_{La}^{2} S k_{CLCD} \rho V^{2}{\left(t \right)} \alpha^{2}{\left(t \right)}}{2} - g m \sin{\left(\gamma{\left(t \right)} \right)} + th \cos{\left(\alpha{\left(t \right)} \right)}}{m}\\\frac{- \frac{S \rho \left(C_{L0} + C_{La} \alpha{\left(t \right)}\right) V^{2}{\left(t \right)}}{2} + g m \cos{\left(\gamma{\left(t \right)} \right)} + m Q{\left(t \right)} V{\left(t \right)} + th \sin{\left(\alpha{\left(t \right)} \right)}}{m V{\left(t \right)}}\\\frac{\frac{S \rho \left(C_{L0} + C_{La} \alpha{\left(t \right)}\right) V^{2}{\left(t \right)}}{2} - g m \cos{\left(\gamma{\left(t \right)} \right)} - th \sin{\left(\alpha{\left(t \right)} \right)}}{m V{\left(t \right)}}\\\frac{S \rho \left(C_{M0} + C_{MQ} Q{\left(t \right)} + C_{Ma} \alpha{\left(t \right)} + C_{Me} elv\right) V^{2}{\left(t \right)}}{2 J_{y}}\end{matrix}\right]$

### Solve Initial Value Problem to Simulate

```python
params = {
    m: 1,
    J_y: 1,
    g: 9.8,
    C_La: 3,
    C_L0: 0.1,
    k_CLCD: 0.1,
    C_D0: 0.03,
    C_M0: 0.01,
    C_Me: 0.1,
    C_MQ: -0.1,
    C_Ma: -0.1, # negative to be stable
    rho: 1.225,
    S: 1,
}
p = sympy.Matrix(list(params.keys()))
p.T
```

$\displaystyle \left[\begin{array}{ccccccccccccc}m & J_{y} & g & C_{La} & C_{L0} & k_{CLCD} & C_{D0} & C_{M0} & C_{Me} & C_{MQ} & C_{Ma} & \rho & S\end{array}\right]$

```python
p0 = [params[k] for k in params.keys()]
p0
```

    [1, 1, 9.8, 3, 0.1, 0.1, 0.03, 0.01, 0.1, -0.1, -0.1, 1.225, 1]

```python
f_eval = sympy.lambdify([[V, alpha, gamma, Q], [th, elv], p], f[:])
f_eval([1, 0, 0, 1], [1, 0], p0)
```

    [0.9810125, 10.738750000000001, -9.738750000000001, -0.05512500000000001]

Make sure your numerical evaluation function is fast.

This function takes 3 arguments, x (states), u (input), p (parameters)

$\dot{x} = f(x, u, p)$

```python
%%timeit -n 10000
f_eval([10, 0, 0, 0], [1, 0], p0)
```

    8.49 µs ± 596 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

```python
def simulate():
    t = np.linspace(0, 20, 1000)
    res = scipy.integrate.solve_ivp(
        fun=lambda t, x: f_eval(x, [1, 0], p0),
        t_span=[t[0], t[-1]], t_eval=t, y0=[6, 0.1, 0, 0])

    plt.figure()
    plt.plot(res.t, res.y.T[:, 0])
    plt.xlabel('t, sec')
    plt.ylabel('V, m/s')
    plt.grid()
    plt.title('Velocity');

    plt.figure()
    plt.plot(res.t, np.rad2deg(res.y.T[:, 1]))
    plt.xlabel('t, sec')
    plt.ylabel('$\\alpha$, deg')
    plt.grid()
    plt.title('Angle of Attack');

    plt.figure()
    plt.plot(res.t, np.rad2deg(res.y.T[:, 2]))
    plt.xlabel('t, sec')
    plt.ylabel('$\\gamma$, deg')
    plt.grid()
    plt.title('Flight Path Angle');

    plt.figure()
    plt.plot(res.t, np.rad2deg(res.y.T[:, 3]))
    plt.xlabel('t, sec')
    plt.ylabel('Q, deg/s')
    plt.grid()
    plt.title('Pitch Rate');
simulate()
```

# Aircraft Trim

Finding equilibrium $x_0$, $u_0$.

```python
# trim cost, penalize state derivatives
trim_cost = sympy.Matrix([
    f[0]**2 + f[1]**2 + f[2]**2 + f[3]**2])

# calculate trim cost
trim_steady_level_obj = sympy.lambdify(
    [[alpha, th, elv], V, params.keys()],
    trim_cost.subs({
        gamma:0,
        Q: 0
    })[0])

# calculate trim cost jacobian
trim_steady_level_jac = sympy.lambdify(
    [[alpha, th, elv], V, params.keys()],
    trim_cost.subs({
        gamma:0,
        Q: 0
    }).jacobian([alpha, th, elv])[:])
```

```python
def do_trim(V_0, p0):
    trim_res = scipy.optimize.minimize(
        fun=lambda x_d: trim_steady_level_obj(
            x_d, V_0, p0),
        # increases solver performance if you can
        # privde jacobian of the cost function, which
        # we can
        jac=lambda x_d: trim_steady_level_jac(
            x_d, V_0, p0),
        x0 = [0, 1, 0],
        tol=np.finfo(float).eps, bounds=[
            [-1, 1],  # alpha bounds, rad
            [0, 5],   # throttle bounds, N
            [-1, 1]], # elevator bounds, rad
        #method='Nelder-Mead'
    )
    print(trim_res)
    alpha_0, th_0, elv_0 = trim_res.x
    trim = {
        V: V_0,
        alpha: alpha_0,
        gamma: 0,
        Q: 0,
        th: th_0,
        elv: elv_0
    }
    return trim

trim = do_trim(V_0=10, p0=p0)
trim
```

          fun: 7.514775069826027e-25
     hess_inv: <3x3 LbfgsInvHessProduct with dtype=float64>
          jac: array([-7.01237904e-12, -1.04825328e-12, -6.94135125e-12])
      message: 'CONVERGENCE: REL_REDUCTION_OF_F_<=_FACTR*EPSMCH'
         nfev: 20
          nit: 15
         njev: 20
       status: 0
      success: True
            x: array([ 0.02021962,  1.99600204, -0.07978038])

    {V(t): 10,
     alpha(t): 0.02021962262929675,
     gamma(t): 0,
     Q(t): 0,
     th: 1.9960020419541868,
     elv: -0.07978037737079575}

```python
A = f.jacobian(x).subs(trim).subs(params)
A
```

$\displaystyle \left[\begin{matrix}-0.399118807992998 & -5.94456905301602 & -9.8 & 0\\-0.196807113162693 & -18.1754405960036 & 0 & 1\\0.196807113162693 & 18.1754405960036 & 0 & 0\\-1.13328183642958 \cdot 10^{-13} & -6.125 & 0 & -6.125\end{matrix}\right]$

```python
B = f.jacobian(u).subs(trim).subs(params)
B
```

$\displaystyle \left[\begin{matrix}0.999795590394625 & 0\\0.00202182449154941 & 0\\-0.00202182449154941 & 0\\0 & 6.125\end{matrix}\right]$

```python
y.T
```

$\displaystyle \left[\begin{matrix}V{\left(t \right)} & \alpha{\left(t \right)} & \gamma{\left(t \right)} & Q{\left(t \right)} & V{\left(t \right)} \sin{\left(\gamma{\left(t \right)} \right)}\end{matrix}\right]$

```python
C = y.jacobian(x).subs(trim).subs(params)
C
```

$\displaystyle \left[\begin{matrix}1 & 0 & 0 & 0\\0 & 1 & 0 & 0\\0 & 0 & 1 & 0\\0 & 0 & 0 & 1\\0 & 0 & 10 & 0\end{matrix}\right]$

```python
D = y.jacobian(u).subs(trim).subs(params)
D
```

$\displaystyle \left[\begin{matrix}0 & 0\\0 & 0\\0 & 0\\0 & 0\\0 & 0\end{matrix}\right]$

Now we can obtain the transfer function matrix relating all inputs to all outputs.

```python
sys_aircraft = control.ss(A, B, C, D)
sys_aircraft
```

\[
\left(\begin{array}{rllrllrllrll|rllrll}
-0.&\hspace{-1em}399&\hspace{-1em}\phantom{\cdot}&-5.&\hspace{-1em}94&\hspace{-1em}\phantom{\cdot}&-9.&\hspace{-1em}8&\hspace{-1em}\phantom{\cdot}&0\phantom{.}&\hspace{-1em}&\hspace{-1em}\phantom{\cdot}&1\phantom{.}&\hspace{-1em}&\hspace{-1em}\phantom{\cdot}&0\phantom{.}&\hspace{-1em}&\hspace{-1em}\phantom{\cdot}\\
-0.&\hspace{-1em}197&\hspace{-1em}\phantom{\cdot}&-18.&\hspace{-1em}2&\hspace{-1em}\phantom{\cdot}&0\phantom{.}&\hspace{-1em}&\hspace{-1em}\phantom{\cdot}&1\phantom{.}&\hspace{-1em}&\hspace{-1em}\phantom{\cdot}&0.&\hspace{-1em}00202&\hspace{-1em}\phantom{\cdot}&0\phantom{.}&\hspace{-1em}&\hspace{-1em}\phantom{\cdot}\\
0.&\hspace{-1em}197&\hspace{-1em}\phantom{\cdot}&18.&\hspace{-1em}2&\hspace{-1em}\phantom{\cdot}&0\phantom{.}&\hspace{-1em}&\hspace{-1em}\phantom{\cdot}&0\phantom{.}&\hspace{-1em}&\hspace{-1em}\phantom{\cdot}&-0.&\hspace{-1em}00202&\hspace{-1em}\phantom{\cdot}&0\phantom{.}&\hspace{-1em}&\hspace{-1em}\phantom{\cdot}\\
-1.&\hspace{-1em}13&\hspace{-1em}\cdot10^{-13}&-6.&\hspace{-1em}12&\hspace{-1em}\phantom{\cdot}&0\phantom{.}&\hspace{-1em}&\hspace{-1em}\phantom{\cdot}&-6.&\hspace{-1em}12&\hspace{-1em}\phantom{\cdot}&0\phantom{.}&\hspace{-1em}&\hspace{-1em}\phantom{\cdot}&6.&\hspace{-1em}12&\hspace{-1em}\phantom{\cdot}\\
\hline
1\phantom{.}&\hspace{-1em}&\hspace{-1em}\phantom{\cdot}&0\phantom{.}&\hspace{-1em}&\hspace{-1em}\phantom{\cdot}&0\phantom{.}&\hspace{-1em}&\hspace{-1em}\phantom{\cdot}&0\phantom{.}&\hspace{-1em}&\hspace{-1em}\phantom{\cdot}&0\phantom{.}&\hspace{-1em}&\hspace{-1em}\phantom{\cdot}&0\phantom{.}&\hspace{-1em}&\hspace{-1em}\phantom{\cdot}\\
0\phantom{.}&\hspace{-1em}&\hspace{-1em}\phantom{\cdot}&1\phantom{.}&\hspace{-1em}&\hspace{-1em}\phantom{\cdot}&0\phantom{.}&\hspace{-1em}&\hspace{-1em}\phantom{\cdot}&0\phantom{.}&\hspace{-1em}&\hspace{-1em}\phantom{\cdot}&0\phantom{.}&\hspace{-1em}&\hspace{-1em}\phantom{\cdot}&0\phantom{.}&\hspace{-1em}&\hspace{-1em}\phantom{\cdot}\\
0\phantom{.}&\hspace{-1em}&\hspace{-1em}\phantom{\cdot}&0\phantom{.}&\hspace{-1em}&\hspace{-1em}\phantom{\cdot}&1\phantom{.}&\hspace{-1em}&\hspace{-1em}\phantom{\cdot}&0\phantom{.}&\hspace{-1em}&\hspace{-1em}\phantom{\cdot}&0\phantom{.}&\hspace{-1em}&\hspace{-1em}\phantom{\cdot}&0\phantom{.}&\hspace{-1em}&\hspace{-1em}\phantom{\cdot}\\
0\phantom{.}&\hspace{-1em}&\hspace{-1em}\phantom{\cdot}&0\phantom{.}&\hspace{-1em}&\hspace{-1em}\phantom{\cdot}&0\phantom{.}&\hspace{-1em}&\hspace{-1em}\phantom{\cdot}&1\phantom{.}&\hspace{-1em}&\hspace{-1em}\phantom{\cdot}&0\phantom{.}&\hspace{-1em}&\hspace{-1em}\phantom{\cdot}&0\phantom{.}&\hspace{-1em}&\hspace{-1em}\phantom{\cdot}\\
0\phantom{.}&\hspace{-1em}&\hspace{-1em}\phantom{\cdot}&0\phantom{.}&\hspace{-1em}&\hspace{-1em}\phantom{\cdot}&10\phantom{.}&\hspace{-1em}&\hspace{-1em}\phantom{\cdot}&0\phantom{.}&\hspace{-1em}&\hspace{-1em}\phantom{\cdot}&0\phantom{.}&\hspace{-1em}&\hspace{-1em}\phantom{\cdot}&0\phantom{.}&\hspace{-1em}&\hspace{-1em}\phantom{\cdot}\\
\end{array}\right)
\]

```python
u.T
```

$\displaystyle \left[\begin{matrix}th & elv\end{matrix}\right]$

```python
y.T
```

$\displaystyle \left[\begin{matrix}V{\left(t \right)} & \alpha{\left(t \right)} & \gamma{\left(t \right)} & Q{\left(t \right)} & V{\left(t \right)} \sin{\left(\gamma{\left(t \right)} \right)}\end{matrix}\right]$

```python
G_aircraft = control.ss2tf(sys_aircraft)
G_aircraft
```

$$\begin{bmatrix}\frac{0.9998 s^3 + 24.3 s^2 + 117.5 s + 0.1214}{s^4 + 24.7 s^3 + 127.9 s^2 + 51.52 s + 11.81}&\frac{1.018 \times 10^{-14} s^2 - 36.41 s - 1091}{s^4 + 24.7 s^3 + 127.9 s^2 + 51.52 s + 11.81}&\\\frac{0.002022 s^3 - 0.1836 s^2 - 1.2 s - 7.105 \times 10^{-15}}{s^4 + 24.7 s^3 + 127.9 s^2 + 51.52 s + 11.81}&\frac{6.125 s^2 + 2.445 s + 11.81}{s^4 + 24.7 s^3 + 127.9 s^2 + 51.52 s + 11.81}&\\\frac{-0.002022 s^3 + 0.1836 s^2 + 1.188 s + 1.2}{s^4 + 24.7 s^3 + 127.9 s^2 + 51.52 s + 11.81}&\frac{-3.153 \times 10^{-17} s^2 + 111.3 s + 37.27}{s^4 + 24.7 s^3 + 127.9 s^2 + 51.52 s + 11.81}&\\\frac{-2.22 \times 10^{-16} s^3 - 0.01238 s^2 + 1.2 s + 3.622 \times 10^{-15}}{s^4 + 24.7 s^3 + 127.9 s^2 + 51.52 s + 11.81}&\frac{6.125 s^3 + 113.8 s^2 + 49.08 s - 2.611 \times 10^{-12}}{s^4 + 24.7 s^3 + 127.9 s^2 + 51.52 s + 11.81}&\\\frac{-0.02022 s^3 + 1.836 s^2 + 11.88 s + 12}{s^4 + 24.7 s^3 + 127.9 s^2 + 51.52 s + 11.81}&\frac{1113 s + 372.7}{s^4 + 24.7 s^3 + 127.9 s^2 + 51.52 s + 11.81}&\\ \end{bmatrix}$$

## Velocity due to Thrust Transfer Function

```python
x.T
```

$\displaystyle \left[\begin{matrix}V{\left(t \right)} & \alpha{\left(t \right)} & \gamma{\left(t \right)} & Q{\left(t \right)}\end{matrix}\right]$

```python
u.T
```

$\displaystyle \left[\begin{matrix}th & elv\end{matrix}\right]$

```python
y.T
```

$\displaystyle \left[\begin{matrix}V{\left(t \right)} & \alpha{\left(t \right)} & \gamma{\left(t \right)} & Q{\left(t \right)} & V{\left(t \right)} \sin{\left(\gamma{\left(t \right)} \right)}\end{matrix}\right]$

```python
def thrust_to_velocity_controller_analysis(G_aircraft):
    print('thrust -> climb rate')
    t = np.linspace(0, 20, 1000)
    tol = 1e-2
    s = control.tf([1, 0], [1])
    G_a = 1/(s/10 + 1) # actuator, first order plant representing electric motor
    G = G_aircraft[4, 0]*G_a  # plant, Q due to d_e * Servo transfer function
    lowpass = 1/(s/30 + 1)
    H = 30 + 15/s + 1*s*lowpass  # PID controller with low pass filter
    G_o = G*H  # open loop
    G_c = control.feedback(G_o) # closed loop
    print('closed loop poles: ', control.pole(G_c))

    r = 0.1*np.ones(len(t))
    res = control.forced_response(G_c, T=t, U=r);
    y = res.y[0, :]

    Kv = np.real(control.minreal(s*G_o)(0))
    print('Kv', Kv)

    print('ess', 1/Kv)
    print('G:')
    print(G)

    print('G_c:')
    print(G_c)
    plt.rcParams['figure.figsize'] = (8, 4)

    plt.figure()
    plt.plot(t, y, label='y') # don't forget these are deviations from trim
    plt.plot(t, r, label='r') # don't forget these are deviations from trim
    plt.hlines(0, t[0], t[-1], linestyles='--', label='trim')
    plt.legend()
    plt.grid()
    plt.xlabel('t, sec')
    plt.ylabel('$\dot{h}$, m/s')
    plt.title('Climb Rate')

    plt.figure()
    e = r - y
    plt.plot(t, e)
    plt.grid()
    plt.xlabel('t, sec')
    plt.ylabel('error, m/s')
    plt.title('Climb Rate Error')

    plt.figure()
    res = control.forced_response(H*G_a, U=e, T=t);
    u = res.y[0, :]

    plt.plot(t, u + trim[th], label='u')  # don't forget these are deviations from trim
    plt.hlines(trim[th], t[0], t[-1], linestyles='--', label='trim')
    plt.legend()
    plt.grid()
    plt.xlabel('t, sec')
    plt.ylabel('thrust, N')
    plt.title('Thrust Output')

    plt.figure()
    control.rlocus(G, xlim=[-4, 1], ylim=[-2, 2])
    rvect, kvect = control.rlocus(G, kvect=[1], plot=False)
    h = plt.plot(np.real(rvect), np.imag(rvect),  marker='s', color='r', alpha=0.5)
    plt.legend([h[0]], ['CL poles'])
    plt.title('uncompensated root locus')

    plt.figure()
    control.rlocus(G*H, xlim=[-4, 1], ylim=[-2, 2])
    rvect, kvect = control.rlocus(G*H, kvect=[1], plot=False)
    h = plt.plot(np.real(rvect), np.imag(rvect),  marker='s', color='r', alpha=0.5)
    plt.legend([h[0]], ['CL poles'])
    plt.title('compensated root locus')
    return locals()

data = thrust_to_velocity_controller_analysis(G_aircraft)
```

```python
G = G_aircraft[2, 1]
G
```

$$\frac{-3.153 \times 10^{-17} s^2 + 111.3 s + 37.27}{s^4 + 24.7 s^3 + 127.9 s^2 + 51.52 s + 11.81}$$

```python
f
```

$\displaystyle \left[\begin{matrix}\frac{- \frac{C_{D0} S \rho V^{2}{\left(t \right)}}{2} - \frac{C_{L0}^{2} S k_{CLCD} \rho V^{2}{\left(t \right)}}{2} - C_{L0} C_{La} S k_{CLCD} \rho V^{2}{\left(t \right)} \alpha{\left(t \right)} - \frac{C_{La}^{2} S k_{CLCD} \rho V^{2}{\left(t \right)} \alpha^{2}{\left(t \right)}}{2} - g m \sin{\left(\gamma{\left(t \right)} \right)} + th \cos{\left(\alpha{\left(t \right)} \right)}}{m}\\\frac{- \frac{S \rho \left(C_{L0} + C_{La} \alpha{\left(t \right)}\right) V^{2}{\left(t \right)}}{2} + g m \cos{\left(\gamma{\left(t \right)} \right)} + m Q{\left(t \right)} V{\left(t \right)} + th \sin{\left(\alpha{\left(t \right)} \right)}}{m V{\left(t \right)}}\\\frac{\frac{S \rho \left(C_{L0} + C_{La} \alpha{\left(t \right)}\right) V^{2}{\left(t \right)}}{2} - g m \cos{\left(\gamma{\left(t \right)} \right)} - th \sin{\left(\alpha{\left(t \right)} \right)}}{m V{\left(t \right)}}\\\frac{S \rho \left(C_{M0} + C_{MQ} Q{\left(t \right)} + C_{Ma} \alpha{\left(t \right)} + C_{Me} elv\right) V^{2}{\left(t \right)}}{2 J_{y}}\end{matrix}\right]$