```python
from IPython.core.display import HTML
def css_styling():
    styles = open("./styles/custom.css", "r").read()
    return HTML(styles)
css_styling()
```

<link href='http://fonts.googleapis.com/css?family=Alegreya+Sans:100,300,400,500,700,800,900,100italic,300italic,400italic,500italic,700italic,800italic,900italic' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Arvo:400,700,400italic' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=PT+Mono' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Shadows+Into+Light' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Philosopher:400,700,400italic,700italic' rel='stylesheet' type='text/css'>

# Shallow Water Equations

As a simple example of solving the Riemann problem for a nonlinear system we look at the *shallow water* equations. These are a simplification of the Navier-Stokes equations, reduced here to one spatial dimension $x$, which determine the height $h(x, t)$ of the water with respect to some reference location, and its velocity $u(x, t)$. In the simplest case (where the bed of the channel is flat, and the gravitational constant is renormalised to $1$) these can be written in the conservation law form
$$
  \partial_t \begin{pmatrix} h \\ h u \end{pmatrix} + \partial_x \begin{pmatrix} hu \\ h u^2 + \tfrac{1}{2} h^2 \end{pmatrix} = {\bf 0}.
$$
The *conserved variables* ${\bf q} = (q_1, q_2)^T = (h, h u)^T$ are effectively the total mass and momentum of the fluid.

## Quasilinear form

As seen in the [theory lesson](Lesson_Theory.ipynb), to construct the solution we need the eigenvalues and eigenvectors of the Jacobian matrix. We can construct them directly, by first noting that, in terms of the conserved variables,
$$
  {\bf f} = \begin{pmatrix} f_1 \\ f_2 \end{pmatrix} = \begin{pmatrix} q_2 \\ \frac{q_2^2}{q_1} + \frac{q_1^2}{2} \end{pmatrix}.
$$
Therefore the Jacobian is
$$  \frac{\partial {\bf f}}{\partial {\bf q}} = \begin{pmatrix} 0 & 1 \\ -u^2 + h & 2 u \end{pmatrix}.
$$
The eigenvalues and eigenvectors follow immediately as
$$
\begin{align}
  \lambda_{1} & = u - \sqrt{h}, & \lambda_{2} & = u + \sqrt{h}, \\
  {\bf r}_{1} & = \begin{pmatrix} 1 \\ u - \sqrt{h} \end{pmatrix}, & {\bf r}_{2} & = \begin{pmatrix} 1 \\ u + \sqrt{h} \end{pmatrix} .
\end{align}
$$
Here we have followed the standard convention $\lambda_1 \le \lambda_2 \le \dots \le \lambda_N$.

An alternative approach that may be considerably easier to apply for more complex problems is to write down a different quasilinear form of the equation, which in this case is in terms of the *primitive variables* ${\bf w} = (h, u)^T$,
$$
  \partial_t \begin{pmatrix} h \\ u \end{pmatrix} + \begin{pmatrix} u & h \\ 1 & u \end{pmatrix} \partial_x \begin{pmatrix} h \\ u \end{pmatrix} = {\bf 0}.
$$
The general form here would be written
$$
  \partial_t {\bf w} + B({\bf w}) \partial_x {\bf w} = {\bf 0}.
$$
It is straightforward to check that
$$
  B = \left( \frac{\partial {\bf q}}{\partial {\bf w}} \right)^{-1} \frac{\partial {\bf f}}{\partial {\bf w}} = \left( \frac{\partial {\bf q}}{\partial {\bf w}} \right)^{-1} \frac{\partial {\bf f}}{\partial {\bf q}} \left( \frac{\partial {\bf q}}{\partial {\bf w}} \right).
$$
Thus $B$ is *similar* to the Jacobian, so must have the same eigenvalues, which is straightforward to check. We also have that
$$
\begin{align}
  B \left\{ \left( \frac{\partial {\bf q}}{\partial {\bf w}} \right)^{-1} {\bf r} \right\} = \lambda \left\{ \left( \frac{\partial {\bf q}}{\partial {\bf w}} \right)^{-1} {\bf r} \right\},
\end{align}
$$
showing that the eigenvectors of the Jacobian can be straightforwardly found from the eigenvectors of $B$, which for the shallow water case are
$$
\begin{align}
  {\bf \hat{r}}_1 &= \begin{pmatrix} -\sqrt{h} \\ 1 \end{pmatrix} & {\bf \hat{r}}_2 &= \begin{pmatrix} \sqrt{h} \\ 1 \end{pmatrix}.
\end{align}
$$

## Rarefaction waves

The solution across a continuous rarefaction wave is given by the solution of the ordinary differential equation
$$
  \partial_{\xi} {\bf q} = \frac{{\bf r}}{{\bf r} \cdot \partial_{{\bf q}} \lambda}
$$
where $\lambda, {\bf r}$ are the eigenvalues and eigenvectors of the Jacobian matrix. Note that we can change variables to get the (physically equivalent) relation differential equation
$$
  \partial_{\xi} {\bf w} = \frac{{\bf r}}{{\bf r} \cdot \partial_{{\bf w}} \lambda}
$$
where now the eigenvectors are those of the appropriate matrix for the quasilinear form for ${\bf w}$. Where ${\bf w}$ are the primitive variables as above, the matrix is $B$ and the eigenvectors given by ${\bf \hat{r}}$ as above.

For the shallow water equations we will solve this equation for the primitive variables for the first wave only - symmetry gives the other wave straightforwardly. Starting from
$$
  \lambda_1 = u - \sqrt{h}, \qquad {\bf \hat{r}}_1 = \begin{pmatrix} -\sqrt{h} \\ 1 \end{pmatrix}
$$
we have
$$
  \partial_{{\bf w}} \lambda_1 = \begin{pmatrix} -\frac{1}{2 \sqrt{h}} \\ 1 \end{pmatrix}
$$
and hence
$$
  {\bf \hat{r}}_1 \cdot \partial_{{\bf w}} \lambda_1 = \frac{3}{2}
$$
from which we have
$$
  \partial_{\xi} \begin{pmatrix} h \\ u \end{pmatrix} = \frac{2}{3} \begin{pmatrix} -\sqrt{h} \\ 1 \end{pmatrix}.
$$

This is straightforwardly integrated to get
$$
  \begin{pmatrix} h \\ u \end{pmatrix} = \begin{pmatrix} \left( c_1 - \frac{\xi}{3} \right)^2 \\ \frac{2}{3} \xi + c_2 \end{pmatrix}.
$$

To fix the integration constants $c_{1,2}$ we need to say which state the solution is starting from. As we are looking at the left wave, we expect it to start from the left state ${\bf w}_l = (h_l, u_l)^T$. The left state will connect to the rarefaction wave when the characteristic speeds match, i.e. when $\xi = \xi_l = \lambda_1 = u_l - \sqrt{h_l}$. Therefore we have
$$
  \begin{pmatrix} h_l \\ u_l \end{pmatrix} = \begin{pmatrix} \left( c_1 - \frac{\xi_l}{3} \right)^2 \\ \frac{2}{3} \xi_l + c_2 \end{pmatrix},
$$
from which we determine
$$
  c_1 = \frac{1}{3} \xi_l + \sqrt{h_l}, \qquad c_2 = u_l - \frac{2}{3} \xi_l.
$$

This gives the final solution
$$
  \begin{pmatrix} h \\ u \end{pmatrix} = \begin{pmatrix} \left( \frac{\xi_l - \xi}{3} + \sqrt{h_l} \right)^2 \\ \frac{2}{3} (\xi - \xi_l) + u_l \end{pmatrix}.
$$

### Rarefaction examples

Let us look at all points that can be connected to a certain state by a rarefaction. We do this in the *phase plane*, which is the $(h, u)$ plane. The "known state" will be given by a marker, and all states along the rarefaction curve given by the line, sometimes known as an integral curve.

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

```python
hl = np.linspace(0.1, 10.1)
ul = np.linspace(-1.0, 1.0)
HL, UL = np.meshgrid(hl, ul)
XIL = UL - np.sqrt(HL)
xi_min = np.min(XIL)
xi_max = np.max(XIL)
h_min = np.min(hl)
h_max = np.max(hl)
u_min = np.min(ul)
u_max = np.max(ul)
xi = np.linspace(xi_min, xi_max)
```

```python
def plot_sw_rarefaction(hl, ul):
    "Plot the rarefaction curve through the state (hl, ul)"

    xil = ul - np.sqrt(hl)
    h = ((xil - xi) / 3.0 + np.sqrt(hl))**2
    u = 2.0 * (xi - xil) / 3.0 + ul

    fig = plt.figure(figsize=(12,8))
    ax = fig.add_subplot(111)
    ax.plot(hl, ul, 'rx', markersize = 16, markeredgewidth = 3)
    ax.plot(h, u, 'k--', linewidth = 2)
    ax.set_xlabel(r"$h$")
    ax.set_ylabel(r"$u$")
    dh = h_max - h_min
    du = u_max - u_min
    ax.set_xbound(h_min - 0.1 * dh, h_max + 0.1 * dh)
    ax.set_ybound(u_min - 0.1 * du, u_max + 0.1 * du)
    fig.tight_layout()
```

```python
from ipywidgets import interactive, FloatSlider

interactive(plot_sw_rarefaction,
            hl = FloatSlider(min = 0.1, max = 10.0, value = 1.0),
            ul = FloatSlider(min = -1.0, max = 1.0, value = 0.0))
```

Failed to display Jupyter Widget of type interactive.

  If you're reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

There is a problem with this: we haven't checked if the states on the curve can be *physically* connected to this point. That is, we haven't checked how the characteristic speed changes along the curve.

Here it is obvious: we know that the characteristics must spread across the rarefaction, so $\lambda$ must increase, and as $\xi = \lambda$ we must have the characteristic coordinate increasing.

```python
def plot_sw_rarefaction_physical(hl, ul):
    "Plot the rarefaction curve through the state (hl, ul)"

    xil = ul - np.sqrt(hl)
    xi_physical = np.linspace(xil, xi_max)
    xi_unphysical = np.linspace(xi_min, xil)
    h_physical = ((xil - xi_physical) / 3.0 + np.sqrt(hl))**2
    u_physical = 2.0 * (xi_physical - xil) / 3.0 + ul
    h_unphysical = ((xil - xi_unphysical) / 3.0 + np.sqrt(hl))**2
    u_unphysical = 2.0 * (xi_unphysical - xil) / 3.0 + ul

    fig = plt.figure(figsize=(12,8))
    ax = fig.add_subplot(111)
    ax.plot(hl, ul, 'rx', markersize = 16, markeredgewidth = 3)
    ax.plot(h_physical, u_physical, 'k-', linewidth = 2, label="Physical")
    ax.plot(h_unphysical, u_unphysical, 'k--', linewidth = 2, label="Unphysical")
    ax.set_xlabel(r"$h$")
    ax.set_ylabel(r"$u$")
    dh = h_max - h_min
    du = u_max - u_min
    ax.set_xbound(h_min - 0.1 * dh, h_max + 0.1 * dh)
    ax.set_ybound(u_min - 0.1 * du, u_max + 0.1 * du)
    ax.legend()
    fig.tight_layout()
```

```python
interactive(plot_sw_rarefaction_physical,
            hl = FloatSlider(min = 0.1, max = 10.0, value = 1.0),
            ul = FloatSlider(min = -1.0, max = 1.0, value = 0.0))
```

Failed to display Jupyter Widget of type interactive.

  If you're reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

We see that along the physical part of the rarefaction curve the height $h$ decreases.

Instead of writing the solution in terms of the similarity coordinate $\xi$ we can instead write the solution in terms of any other single parameter. It is useful to write it in terms of the height, which can be done simply by re-arranging the equations giving $u$ and $h$ in terms of $\xi$. So, a state with height $h_m$ to the right of the state $(h_l, u_l)$ can be connected across a rarefaction if
$$
  u_m = u_l + 2 \left( \sqrt{h_l} - \sqrt{h_m} \right).
$$

In this form we will look at the characteristic curves and the behaviour in state space to cross-check.

```python
def plot_sw_rarefaction_physical_characteristics(hl, ul, hm):
    "Plot the rarefaction curve through the state (hl, ul) finishing at (hm, um)"

    um = ul + 2.0 * (np.sqrt(hl) - np.sqrt(hm))

    h_maximum = np.max([h_max, hl, hm])
    h_minimum = np.min([h_min, hl, hm])
    u_maximum = np.max([u_max, ul, um])
    u_minimum = np.min([u_min, ul, um])
    dh = h_maximum - h_minimum
    du = u_maximum - u_minimum
    xi_min = u_minimum - np.sqrt(h_maximum)
    xi_max = u_maximum - np.sqrt(h_minimum)

    xil = ul - np.sqrt(hl)
    xim = um - np.sqrt(hm)
    xi_physical = np.linspace(xil, xi_max)
    xi_unphysical = np.linspace(xi_min, xil)
    h_physical = ((xil - xi_physical) / 3.0 + np.sqrt(hl))**2
    u_physical = 2.0 * (xi_physical - xil) / 3.0 + ul
    h_unphysical = ((xil - xi_unphysical) / 3.0 + np.sqrt(hl))**2
    u_unphysical = 2.0 * (xi_unphysical - xil) / 3.0 + ul

    fig = plt.figure(figsize=(12,8))
    ax1 = fig.add_subplot(121)
    ax1.plot(hl, ul, 'rx', markersize = 16, markeredgewidth = 3, label=r"$(h_l, u_l)$")
    ax1.plot(hm, um, 'b+', markersize = 16, markeredgewidth = 3, label=r"$(h_m, u_m)$")
    ax1.plot(h_physical, u_physical, 'k-', linewidth = 2, label="Physical")
    ax1.plot(h_unphysical, u_unphysical, 'k--', linewidth = 2, label="Unphysical")
    ax1.set_xlabel(r"$h$")
    ax1.set_ylabel(r"$u$")
    ax1.set_xbound(h_minimum - 0.1 * dh, h_maximum + 0.1 * dh)
    ax1.set_ybound(u_minimum - 0.1 * du, u_maximum + 0.1 * du)
    ax1.legend()

    ax2 = fig.add_subplot(122)
    left_edge = np.min([-1.0, -1.0 - xil])
    right_edge = np.max([1.0, 1.0 - xim])
    x_start_points_l = np.linspace(left_edge, 0.0, 20)
    x_start_points_r = np.linspace(0.0, right_edge, 20)
    x_end_points_l = x_start_points_l + xil
    x_end_points_r = x_start_points_r + xim

    for xs, xe in zip(x_start_points_l, x_end_points_l):
        ax2.plot([xs, xe], [0.0, 1.0], 'b-')
    for xs, xe in zip(x_start_points_r, x_end_points_r):
        ax2.plot([xs, xe], [0.0, 1.0], 'g-')

    # Rarefaction wave
    if (xim > xil):
        xi = np.linspace(xil, xim, 11)
        x_end_rarefaction = xi
        for xe in x_end_rarefaction:
            ax2.plot([0.0, xe], [0.0, 1.0], 'r--')
    else:
        x_fill = [x_end_points_l[-1], x_start_points_l[-1], x_end_points_r[0]]
        t_fill = [1.0, 0.0, 1.0]
        ax2.fill_between(x_fill, t_fill, 1.0, facecolor = 'red', alpha = 0.5)

    ax2.set_xbound(-1.0, 1.0)
    ax2.set_ybound(0.0, 1.0)
    ax2.set_xlabel(r"$x$")
    ax2.set_ylabel(r"$t$")
    fig.tight_layout()
```

```python
interactive(plot_sw_rarefaction_physical_characteristics,
            hl = FloatSlider(min = 0.1, max = 10.0, value = 1.0),
            ul = FloatSlider(min = -1.0, max = 1.0, value = 0.0),
            hm = FloatSlider(min = 0.1, max = 10.0, value = 0.5))
```

Failed to display Jupyter Widget of type interactive.

  If you're reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

We clearly see that only if $h_m < h_l$ do the characteristics spread as they should for a rarefaction. This is, in fact, already given by results above: we showed that $\partial_{\xi} h \propto -\sqrt{h}$. As the height $h$ is positive, this means that as $\xi$ increase across the rarefaction, the height must decrease.

## All rarefaction solution

The above exercise assumed we knew the left state and found all right states connecting it by a rarefaction. Now we assume we know both left *and* right states, and assume they connect to a central state, *both* along rarefactions.

First, we need to find which states will connect to the right state across a rarefaction.

### Exercise

Repeat the above calculations for states connecting to a known right state. That is, show that, given the right state $(h_r, u_r)$, the left state that connects to it across a rarefaction satisfies
$$
  \begin{pmatrix} h \\ u \end{pmatrix} = \begin{pmatrix} \left( -\frac{\xi_r - \xi}{3} + \sqrt{h_r} \right)^2 \\ \frac{2}{3} (\xi - \xi_r) + u_r \end{pmatrix}.
$$
or equivalently, given $h_m$, that
$$
  u_m = u_r - 2 \left( \sqrt{h_r} - \sqrt{h_m} \right).
$$
Also check that $h$ decreases across the rarefaction, so for a physical solution $h_m < h_r$.

Then we can plot the curve of all states that can be connected to $(h_l, u_l)$ across a left rarefaction, and the curve of all states that can be connected to $(h_r, u_r)$ across a right rarefaction. *If* they intersect along the *physical* part of the curve, then we have the solution to the Riemann problem. Clearly this only occurs if $h_m < h_l$ *and* $h_m < h_r$.

In this case (and note that this is a special case!) we can solve it analytically. We note that, using our *assumption* that both curves are rarefactions, we have that
$$
\begin{align}
  u_m & = u_l + 2 \left( \sqrt{h_l} - \sqrt{h_m} \right) \\
   & = u_r - 2 \left( \sqrt{h_r} - \sqrt{h_m} \right)
\end{align}
$$
Therefore we have
$$
  h_m = \frac{1}{16} \left( u_l - u_r + 2 \left( \sqrt{h_l} + \sqrt{h_r} \right) \right)^2.
$$

```python
def plot_sw_all_rarefaction(hl, ul, hr, ur):
    "Plot the all rarefaction solution curve for states (hl, ul) and (hr, ur)"

    hm = (ul - ur + 2.0 * (np.sqrt(hl) + np.sqrt(hr)))**2 / 16.0
    um = ul + 2.0 * (np.sqrt(hl) - np.sqrt(hm))

    h_maximum = np.max([h_max, hl, hr, hm])
    h_minimum = np.min([h_min, hl, hr, hm])
    u_maximum = np.max([u_max, ul, ur, um])
    u_minimum = np.min([u_min, ul, ur, um])
    dh = h_maximum - h_minimum
    du = u_maximum - u_minimum
    xil_min = u_minimum - np.sqrt(h_maximum)
    xil_max = u_maximum - np.sqrt(h_minimum)
    xir_min = u_minimum + np.sqrt(h_minimum)
    xir_max = u_maximum + np.sqrt(h_maximum)

    xil = ul - np.sqrt(hl)
    xilm = um - np.sqrt(hm)
    xil_physical = np.linspace(xil, xil_max)
    xil_unphysical = np.linspace(xil_min, xil)
    hl_physical = ((xil - xil_physical) / 3.0 + np.sqrt(hl))**2
    ul_physical = 2.0 * (xil_physical - xil) / 3.0 + ul
    hl_unphysical = ((xil - xil_unphysical) / 3.0 + np.sqrt(hl))**2
    ul_unphysical = 2.0 * (xil_unphysical - xil) / 3.0 + ul

    xir = ur + np.sqrt(hr)
    xirm = um + np.sqrt(hm)
    xir_unphysical = np.linspace(xir, xir_max)
    xir_physical = np.linspace(xir_min, xir)
    hr_physical = (-(xir - xir_physical) / 3.0 + np.sqrt(hr))**2
    ur_physical = 2.0 * (xir_physical - xir) / 3.0 + ur
    hr_unphysical = (-(xir - xir_unphysical) / 3.0 + np.sqrt(hr))**2
    ur_unphysical = 2.0 * (xir_unphysical - xir) / 3.0 + ur

    fig = plt.figure(figsize=(12,8))
    ax1 = fig.add_subplot(111)
    if (hm < np.min([hl, hr])):
        ax1.plot(hm, um, 'b+', markersize = 16, markeredgewidth = 3,
                 label=r"$(h_m, u_m)$, physical solution")
    else:
        ax1.plot(hm, um, 'b+', markersize = 16, markeredgewidth = 3,
                 label=r"$(h_m, u_m)$, not physical solution")
    ax1.plot(hl, ul, 'rx', markersize = 16, markeredgewidth = 3, label=r"$(h_l, u_l)$")
    ax1.plot(hr, ur, 'go', markersize = 16, markeredgewidth = 3, label=r"$(h_r, u_r)$")
    ax1.plot(hl_physical, ul_physical, 'k-', linewidth = 2, label="Physical (left)")
    ax1.plot(hl_unphysical, ul_unphysical, 'k--', linewidth = 2, label="Unphysical (left)")
    ax1.plot(hr_physical, ur_physical, 'c-', linewidth = 2, label="Physical (right)")
    ax1.plot(hr_unphysical, ur_unphysical, 'c--', linewidth = 2, label="Unphysical (right)")
    ax1.set_xlabel(r"$h$")
    ax1.set_ylabel(r"$u$")
    ax1.set_xbound(h_minimum - 0.1 * dh, h_maximum + 0.1 * dh)
    ax1.set_ybound(u_minimum - 0.1 * du, u_maximum + 0.1 * du)
    ax1.legend()

    fig.tight_layout()
```

```python
interactive(plot_sw_all_rarefaction,
            hl = FloatSlider(min = 0.1, max = 10.0, value = 1.0),
            ul = FloatSlider(min = -1.0, max = 1.0, value = -0.5),
            hr = FloatSlider(min = 0.1, max = 10.0, value = 1.0),
            ur = FloatSlider(min = -1.0, max = 1.0, value = 0.5))
```

Failed to display Jupyter Widget of type interactive.

  If you're reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

Given the central state and the relation along rarefaction curves, we can then construct the characteristics and the solution in terms of the similarity coordinate (which, given a time $t$, gives the solution as a function of $x$).

```python
def plot_sw_all_rarefaction_solution(hl, ul, hr, ur):
    "Plot the all rarefaction solution curve for states (hl, ul) and (hr, ur)"

    hm = (ul - ur + 2.0 * (np.sqrt(hl) + np.sqrt(hr)))**2 / 16.0
    um = ul + 2.0 * (np.sqrt(hl) - np.sqrt(hm))

    xi1l = ul - np.sqrt(hl)
    xi1m = um - np.sqrt(hm)
    xi1r = ur - np.sqrt(hr)
    hl_raref = np.linspace(hl, hm, 20)
    ul_raref = ul + 2.0 * (np.sqrt(hl) - np.sqrt(hl_raref))
    xil_raref = ul_raref - np.sqrt(hl_raref)

    xi2r = ur + np.sqrt(hr)
    xi2m = um + np.sqrt(hm)
    xi2l = ul + np.sqrt(hl)
    hr_raref = np.linspace(hm, hr)
    ur_raref = ur - 2.0 * (np.sqrt(hr) - np.sqrt(hr_raref))
    xir_raref = ur_raref + np.sqrt(hr_raref)

    xi_min = np.min([-1.0, xi1l, xi1m, xi2r, xi2m])
    xi_max = np.max([1.0, xi1l, xi1m, xi2r, xi2m])
    d_xi = xi_max - xi_min
    h_max = np.max([hl, hr, hm])
    h_min = np.min([hl, hr, hm])
    d_h = h_max - h_min
    u_max = np.max([ul, ur, um])
    u_min = np.min([ul, ur, um])
    d_u = u_max - u_min

    xi = np.array([xi_min - 0.1 * d_xi, xi1l])
    h = np.array([hl, hl])
    u = np.array([ul, ul])
    xi = np.append(xi, xil_raref)
    h = np.append(h, hl_raref)
    u = np.append(u, ul_raref)
    xi = np.append(xi, [xi1m, xi2m])
    h = np.append(h, [hm, hm])
    u = np.append(u, [um, um])
    xi = np.append(xi, xir_raref)
    h = np.append(h, hr_raref)
    u = np.append(u, ur_raref)
    xi = np.append(xi, [xi2r, xi_max + 0.1 * d_xi])
    h = np.append(h, [hr, hr])
    u = np.append(u, [ur, ur])

    fig = plt.figure(figsize=(12,8))
    ax1 = fig.add_subplot(221)
    if (hm < np.min([hl, hr])):
        ax1.plot(xi, h, 'b-', label = "Physical solution")
    else:
        ax1.plot(xi, h, 'r--', label = "Unphysical solution")
    ax1.set_ybound(h_min - 0.1 * d_h, h_max + 0.1 * d_h)
    ax1.set_xlabel(r"$\xi$")
    ax1.set_ylabel(r"$h$")
    ax1.legend()
    ax2 = fig.add_subplot(222)
    if (hm < np.min([hl, hr])):
        ax2.plot(xi, u, 'b-', label = "Physical solution")
    else:
        ax2.plot(xi, u, 'r--', label = "Unphysical solution")
    ax2.set_ybound(u_min - 0.1 * d_u, u_max + 0.1 * d_u)
    ax2.set_xlabel(r"$\xi$")
    ax2.set_ylabel(r"$u$")
    ax2.legend()

    ax3 = fig.add_subplot(223)
    left_end = np.min([-1.0, 1.1*xi1l])
    right_end = np.max([1.0, 1.1*xi2r])
    left_edge = left_end - xi1l
    right_edge = right_end - xi1r
    x1_start_points_l = np.linspace(np.min([left_edge, left_end]), 0.0, 20)
    x1_start_points_r = np.linspace(0.0, np.max([right_edge, right_end]), 20)
    x1_end_points_l = x1_start_points_l + xi1l
    t1_end_points_r = np.ones_like(x1_start_points_r)

    # Look for intersections
    t1_end_points_r = np.minimum(t1_end_points_r, x1_start_points_r / (xi2r - xi1r))
    x1_end_points_r = x1_start_points_r + xi1r * t1_end_points_r
    # Note: here we are cheating, and using the characteristic speed of the middle state,
    # ignoring howo it varies across the rarefaction
    x1_final_points_r = x1_end_points_r + (1.0 - t1_end_points_r) * xi1m

    for xs, xe in zip(x1_start_points_l, x1_end_points_l):
        ax3.plot([xs, xe], [0.0, 1.0], 'b-')
    for xs, xe, te in zip(x1_start_points_r, x1_end_points_r, t1_end_points_r):
        ax3.plot([xs, xe], [0.0, te], 'g-')
    for xs, xe, ts in zip(x1_end_points_r, x1_final_points_r, t1_end_points_r):
        ax3.plot([xs, xe], [ts, 1.0], 'g-')

    # Highlight the edges of both rarefactions
    ax3.plot([0.0, xi1l], [0.0, 1.0], 'r-', linewidth=2)
    ax3.plot([0.0, xi1m], [0.0, 1.0], 'r-', linewidth=2)
    ax3.plot([0.0, xi2m], [0.0, 1.0], 'r-', linewidth=2)
    ax3.plot([0.0, xi2r], [0.0, 1.0], 'r-', linewidth=2)

    # Rarefaction wave
    if (xi1l < xi1m):
        xi = np.linspace(xi1l, xi1m, 11)
        x_end_rarefaction = xi
        for xe in x_end_rarefaction:
            ax3.plot([0.0, xe], [0.0, 1.0], 'r--')
    else:
        x_fill = [xi1l, 0.0, xi1m]
        t_fill = [1.0, 0.0, 1.0]
        ax3.fill_between(x_fill, t_fill, 1.0, facecolor = 'red', alpha = 0.5)

    ax3.set_xlabel(r"$x$")
    ax3.set_ylabel(r"$t$")
    ax3.set_title("1-characteristics")
    ax3.set_xbound(left_end, right_end)

    ax4 = fig.add_subplot(224)
    left_end = np.min([-1.0, 1.1*xi1l])
    right_end = np.max([1.0, 1.1*xi2r])
    left_edge = left_end - xi2l
    right_edge = right_end - xi2r
    x2_start_points_l = np.linspace(np.min([left_edge, left_end]), 0.0, 20)
    x2_start_points_r = np.linspace(0.0, np.max([right_edge, right_end]), 20)
    x2_end_points_r = x2_start_points_r + xi2r
    t2_end_points_l = np.ones_like(x2_start_points_l)

    # Look for intersections
    t2_end_points_l = np.minimum(t2_end_points_l, x2_start_points_l / (xi1l - xi2r))
    x2_end_points_l = x2_start_points_l + xi2r * t2_end_points_l
    # Note: here we are cheating, and using the characteristic speed of the middle state,
    # ignoring howo it varies across the rarefaction
    x2_final_points_l = x2_end_points_l + (1.0 - t2_end_points_l) * xi2m

    for xs, xe in zip(x2_start_points_r, x2_end_points_r):
        ax4.plot([xs, xe], [0.0, 1.0], 'g-')
    for xs, xe, te in zip(x2_start_points_l, x2_end_points_l, t2_end_points_l):
        ax4.plot([xs, xe], [0.0, te], 'b-')
    for xs, xe, ts in zip(x2_end_points_l, x2_final_points_l, t2_end_points_l):
        ax4.plot([xs, xe], [ts, 1.0], 'b-')

    # Highlight the edges of both rarefactions
    ax4.plot([0.0, xi1l], [0.0, 1.0], 'r-', linewidth=2)
    ax4.plot([0.0, xi1m], [0.0, 1.0], 'r-', linewidth=2)
    ax4.plot([0.0, xi2m], [0.0, 1.0], 'r-', linewidth=2)
    ax4.plot([0.0, xi2r], [0.0, 1.0], 'r-', linewidth=2)

    # Rarefaction wave
    if (xi2r > xi2m):
        xi = np.linspace(xi2m, xi2r, 11)
        x_end_rarefaction = xi
        for xe in x_end_rarefaction:
            ax4.plot([0.0, xe], [0.0, 1.0], 'r--')
    else:
        x_fill = [xi2m, 0.0, xi2r]
        t_fill = [1.0, 0.0, 1.0]
        ax4.fill_between(x_fill, t_fill, 1.0, facecolor = 'red', alpha = 0.5)

    ax4.set_xlabel(r"$x$")
    ax4.set_ylabel(r"$t$")
    ax4.set_title("2-characteristics")
    ax4.set_xbound(left_end, right_end)

    fig.tight_layout()
```

```python
interactive(plot_sw_all_rarefaction_solution,
            hl = FloatSlider(min = 0.1, max = 10.0, value = 1.0),
            ul = FloatSlider(min = -1.0, max = 1.0, value = -0.5),
            hr = FloatSlider(min = 0.1, max = 10.0, value = 1.0),
            ur = FloatSlider(min = -1.0, max = 1.0, value = 0.5))
```

Failed to display Jupyter Widget of type interactive.

  If you're reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

## Shocks

We note that the [general theory](Lesson_Theory.ipynb) tells us that across a shock the Rankine-Hugoniot conditions
$$
  V_s \left[ {\bf q} \right] =  \left[ {\bf f}({\bf q}) \right]
$$
must be satisfied.

For the shallow water equations we will start, as with the rarefaction case, by assuming we know the left state ${\bf q}_l = (h_l, u_l)$, and work out which states ${\bf q}_m$ can be connected to it across a shock.

Note here that the procedure is *identical* for the right state as the direction does not matter. However, there will be multiple solutions, and checking which is physically correct does require checking whether the left or the right state is known

Writing out the conditions in full we see that
$$
\begin{align}
  V_s \left( h_m - h_l \right) & = h_m u_m - h_l u_l \\
  V_s \left( h_m u_m - h_l u_l \right) & = h_m u_m^2 + \tfrac{1}{2} h_m^2 - h_l u_l^2 - \tfrac{1}{2} h_l^2
\end{align}
$$

Eliminating the shock speed $V_s$ gives, using the second equation,
$$
  u_m^2 - (2 u_l) u_m + \left[ u_l^2 - \tfrac{1}{2} \left( h_l - h_m \right) \left( \frac{h_l}{h_m} - \frac{h_m}{h_l} \right) \right] = 0.
$$
This has the solutions (assuming that $h_m$ is known!)
$$
  u_m = u_l \pm \sqrt{\tfrac{1}{2} \left( h_l - h_m \right) \left( \frac{h_l}{h_m} - \frac{h_m}{h_l} \right)}.
$$

We can again use the Rankine-Hugoniot relations to find the shock speed.
$$
  V_s = u_l \pm \frac{h_m}{h_m - h_l} \sqrt{\tfrac{1}{2} \left( h_l - h_m \right) \left( \frac{h_l}{h_m} - \frac{h_m}{h_l} \right)}.
$$

We should at this point find which sign is appropriate. Comparing the shock speeds against the characteristic speed will show that

* we need $h_m > h_l$ for the wave to be a shock, and
* we take the negative sign if connected to a left state, and the positive if connected to a right state.

However, we can see this by plotting the *Hugoniot locus*: the curve of all states that can be connected to $(h_l, u_l)$ across a shock.

```python
def plot_sw_shock_physical(hl, ul):
    "Plot the shock curve through the state (hl, ul)"

    h = np.linspace(h_min, h_max, 500)
    u_negative = ul - np.sqrt(0.5 * (hl - h) * (hl / h - h / hl))
    u_positive = ul + np.sqrt(0.5 * (hl - h) * (hl / h - h / hl))

    vs_negative = ul - h / (h - hl) * np.sqrt(0.5 * (hl - h) * (hl / h - h / hl))
    vs_positive = ul + h / (h - hl) * np.sqrt(0.5 * (hl - h) * (hl / h - h / hl))

    xi1_negative = u_negative - np.sqrt(h)
    xi1_positive = u_positive - np.sqrt(h)
    xi2_negative = u_negative + np.sqrt(h)
    xi2_positive = u_positive + np.sqrt(h)

    xi1_l = ul - np.sqrt(hl)
    xi2_l = ul + np.sqrt(hl)

    h1_physical = h[np.logical_and(xi1_negative <= vs_negative, xi1_l >= vs_negative)]
    u1_physical = u_negative[np.logical_and(xi1_negative <= vs_negative, xi1_l >= vs_negative)]
    h2_physical = h[np.logical_and(xi2_positive >= vs_positive, xi2_l <= vs_positive)]
    u2_physical = u_positive[np.logical_and(xi2_positive >= vs_positive, xi2_l <= vs_positive)]
    h1_unphysical = h[np.logical_or(xi1_negative >= vs_negative, xi1_l <= vs_negative)]
    u1_unphysical = u_negative[np.logical_or(xi1_negative >= vs_negative, xi1_l <= vs_negative)]
    h2_unphysical = h[np.logical_or(xi2_positive <= vs_positive, xi2_l >= vs_positive)]
    u2_unphysical = u_positive[np.logical_or(xi2_positive <= vs_positive, xi2_l >= vs_positive)]

    fig = plt.figure(figsize=(12,8))
    ax = fig.add_subplot(111)
    ax.plot(hl, ul, 'rx', markersize = 16, markeredgewidth = 3)
    ax.plot(h1_physical, u1_physical, 'b-', linewidth = 2,
            label="Physical, 1-shock")
    ax.plot(h1_unphysical, u1_unphysical, 'b--', linewidth = 2,
            label="Unphysical, 1-shock")
    ax.plot(h2_physical, u2_physical, 'g-', linewidth = 2,
            label="Physical, 2-shock")
    ax.plot(h2_unphysical, u2_unphysical, 'g--', linewidth = 2,
            label="Unphysical, 2-shock")
    ax.plot(h[::5], u_negative[::5], 'co', markersize = 12, markeredgewidth = 2, alpha = 0.3,
            label="Negative branch")
    ax.plot(h[::5], u_positive[::5], 'ro', markersize = 12, markeredgewidth = 2, alpha = 0.3,
            label="Positive branch")
    ax.set_xlabel(r"$h$")
    ax.set_ylabel(r"$u$")
    dh = h_max - h_min
    du = u_max - u_min
    ax.set_xbound(h_min, h_max)
    ax.set_ybound(u_min, u_max)
    ax.legend()
    fig.tight_layout()
```

```python
interactive(plot_sw_shock_physical,
            hl = FloatSlider(min = 0.1, max = 10.0, value = 1.0),
            ul = FloatSlider(min = -1.0, max = 1.0, value = 0.0))
```

Failed to display Jupyter Widget of type interactive.

  If you're reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

We see from these results, as claimed above, that

* we need $h_m > h_l$ (or $h_m > h_r$) for the wave to be a shock, and
* we take the negative sign if connected to a left state, and the positive if connected to a right state.

## All shock solution

When we assumed the solution contained two rarefactions it was possible to write the full solution in closed form. If we assume the solution contains two shocks then it is not possible to do this. However, it is straightforward to find the solution numerically.

We assume the left state ${\bf w}_l = (h_l, u_l)$ and the right state ${\bf w}_r = (h_r, u_r)$ are known, and that they both connect to the central state ${\bf w}_m = (h_m, u_m)$ through shocks. We know that
$$
\begin{align}
  u_m & = u_l - \sqrt{\tfrac{1}{2} \left( h_l - h_m \right) \left( \frac{h_l}{h_m} - \frac{h_m}{h_l} \right)}, \\
  u_m & = u_r + \sqrt{\tfrac{1}{2} \left( h_r - h_m \right) \left( \frac{h_r}{h_m} - \frac{h_m}{h_r} \right)}.
\end{align}
$$
We schematically write these equations as
$$
\begin{align}
  u_m & = \phi_l \left( h_m; {\bf w}_l \right), \\
  u_m & = \phi_r \left( h_m; {\bf w}_r \right),
\end{align}
$$
to indicate that the velocity in the central state, $u_m$, can be written as a function of the single unknown $h_m$ and known data.

We immediately see that $h_m$ is a root of the nonlinear equation
$$
  \phi \left( h_m; {\bf w}_l, {\bf w}_r \right) = \phi_l \left( h_m; {\bf w}_l \right) - \phi_r \left( h_m; {\bf w}_r \right) = 0.
$$

Finding the roots of scalar nonlinear equations is a standard problem in numerical methods, with methods such as bisection, Newton-Raphson and more being well-known. `scipy` provides a number of standard algorithms - here we will use the recommended `brentq` method.

Note that as soon as we have numerically determined $h_m$ then either formula above gives $u_m$, and the shock speeds follow.

```python
def plot_sw_all_shock(hl, ul, hr, ur):
    "Plot the all shock solution curve for states (hl, ul) and (hr, ur)"

    from scipy.optimize import brentq

    def phi(hstar):
        "Function defining the root"

        phi_l = ul - np.sqrt(0.5 * (hl - hstar) * (hl / hstar - hstar / hl))
        phi_r = ur + np.sqrt(0.5 * (hr - hstar) * (hr / hstar - hstar / hr))

        return phi_l - phi_r

    # There is a solution only in the physical case.
    physical_solution = True
    try:
        hm = brentq(phi, np.max([hl, hr]), 10.0 * h_max)
    except ValueError:
        physical_solution = False
        hm = hl
    um = ul - np.sqrt(0.5 * (hl - hm) * (hl / hm - hm / hl))

    h = np.linspace(h_min, h_max, 500)
    u_negative = ul - np.sqrt(0.5 * (hl - h) * (hl / h - h / hl))
    u_positive = ur + np.sqrt(0.5 * (hr - h) * (hr / h - h / hr))

    h_maximum = np.max([h_max, hl, hr, hm])
    h_minimum = np.min([h_min, hl, hr, hm])
    u_maximum = np.max([u_max, ul, ur, um])
    u_minimum = np.min([u_min, ul, ur, um])
    dh = h_maximum - h_minimum
    du = u_maximum - u_minimum
    xil_min = u_minimum - np.sqrt(h_maximum)
    xil_max = u_maximum - np.sqrt(h_minimum)
    xir_min = u_minimum + np.sqrt(h_minimum)
    xir_max = u_maximum + np.sqrt(h_maximum)

    vs_negative = ul - h / (h - hl) * np.sqrt(0.5 * (hl - h) * (hl / h - h / hl))
    vs_positive = ur + h / (h - hr) * np.sqrt(0.5 * (hr - h) * (hr / h - h / hr))

    xi1_negative = u_negative - np.sqrt(h)
    xi1_positive = u_positive - np.sqrt(h)
    xi2_negative = u_negative + np.sqrt(h)
    xi2_positive = u_positive + np.sqrt(h)

    xi1_l = ul - np.sqrt(hl)
    xi2_r = ur + np.sqrt(hr)

    h1_physical = h[np.logical_and(xi1_negative <= vs_negative, xi1_l >= vs_negative)]
    u1_physical = u_negative[np.logical_and(xi1_negative <= vs_negative, xi1_l >= vs_negative)]
    h2_physical = h[np.logical_and(xi2_positive >= vs_positive, xi2_r <= vs_positive)]
    u2_physical = u_positive[np.logical_and(xi2_positive >= vs_positive, xi2_r <= vs_positive)]
    h1_unphysical = h[np.logical_or(xi1_negative >= vs_negative, xi1_l <= vs_negative)]
    u1_unphysical = u_negative[np.logical_or(xi1_negative >= vs_negative, xi1_l <= vs_negative)]
    h2_unphysical = h[np.logical_or(xi2_positive <= vs_positive, xi2_r >= vs_positive)]
    u2_unphysical = u_positive[np.logical_or(xi2_positive <= vs_positive, xi2_r >= vs_positive)]

    fig = plt.figure(figsize=(12,8))
    ax = fig.add_subplot(111)
    ax.plot(hl, ul, 'rx', markersize = 16, markeredgewidth = 3, label = r"${\bf w}_l$")
    ax.plot(hr, ur, 'r+', markersize = 16, markeredgewidth = 3, label = r"${\bf w}_r$")
    if physical_solution:
        ax.plot(hm, um, 'ro', markersize = 16, markeredgewidth = 3, label = r"${\bf w}_m$")
    ax.plot(h1_physical, u1_physical, 'b-', linewidth = 2,
            label="Physical, 1-shock")
    ax.plot(h1_unphysical, u1_unphysical, 'b--', linewidth = 2,
            label="Unphysical, 1-shock")
    ax.plot(h2_physical, u2_physical, 'g-', linewidth = 2,
            label="Physical, 2-shock")
    ax.plot(h2_unphysical, u2_unphysical, 'g--', linewidth = 2,
            label="Unphysical, 2-shock")
    ax.set_xlabel(r"$h$")
    ax.set_ylabel(r"$u$")
    ax.set_xbound(h_minimum - 0.1 * dh, h_maximum + 0.1 * dh)
    ax.set_ybound(u_minimum - 0.1 * du, u_maximum + 0.1 * du)
    ax.legend()

    fig.tight_layout()
```

```python
interactive(plot_sw_all_shock,
            hl = FloatSlider(min = 0.1, max = 10.0, value = 1.0),
            ul = FloatSlider(min = -1.0, max = 1.0, value = 0.2),
            hr = FloatSlider(min = 0.1, max = 10.0, value = 1.0),
            ur = FloatSlider(min = -1.0, max = 1.0, value = -0.2))
```

Failed to display Jupyter Widget of type interactive.

  If you're reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

Finally, we can plot the solution in physical space.

```python
def plot_sw_all_shock_solution(hl, ul, hr, ur):
    "Plot the all shock solution for states (hl, ul) and (hr, ur)"

    from scipy.optimize import brentq

    def phi(hstar):
        "Function defining the root"

        phi_l = ul - np.sqrt(0.5 * (hl - hstar) * (hl / hstar - hstar / hl))
        phi_r = ur + np.sqrt(0.5 * (hr - hstar) * (hr / hstar - hstar / hr))

        return phi_l - phi_r

    # There is a solution only in the physical case.
    physical_solution = True
    try:
        hm = brentq(phi, np.max([hl, hr]), 10.0 * h_max)
    except ValueError:
        physical_solution = False
        hm = hl
    um = ul - np.sqrt(0.5 * (hl - hm) * (hl / hm - hm / hl))

    xi1l = ul - np.sqrt(hl)
    xi1m = um - np.sqrt(hm)
    xi1r = ur - np.sqrt(hr)
    if physical_solution:
        vsl = ul - hm / (hm - hl) * np.sqrt(0.5 * (hl - hm) * (hl / hm - hm / hl))
    else:
        vsl = xi1l

    xi2r = ur + np.sqrt(hr)
    xi2m = um + np.sqrt(hm)
    xi2l = ul + np.sqrt(hl)
    if physical_solution:
        vsr = ur + hm / (hm - hr) * np.sqrt(0.5 * (hr - hm) * (hr / hm - hm / hr))
    else:
        vsr = xi2r

    xi_min = np.min([-1.0, xi1l, xi1m, xi2r, xi2m])
    xi_max = np.max([1.0, xi1l, xi1m, xi2r, xi2m])
    d_xi = xi_max - xi_min
    h_maximum = np.max([hl, hr, hm])
    h_minimum = np.min([hl, hr, hm])
    d_h = h_maximum - h_minimum
    u_maximum = np.max([ul, ur, um])
    u_minimum = np.min([ul, ur, um])
    d_u = u_maximum - u_minimum

    xi = np.array([xi_min - 0.1 * d_xi, vsl, vsl, vsr, vsr, xi_max + 0.1 * d_xi])
    h = np.array([hl, hl, hm, hm, hr, hr])
    u = np.array([ul, ul, um, um, ur, ur])

    fig = plt.figure(figsize=(12,8))
    ax1 = fig.add_subplot(221)
    if (hm > np.max([hl, hr])):
        ax1.plot(xi, h, 'b-', label = "Physical solution")
    else:
        ax1.plot(xi, h, 'r--', label = "Unphysical solution")
    ax1.set_ybound(h_minimum - 0.1 * d_h, h_maximum + 0.1 * d_h)
    ax1.set_xlabel(r"$\xi$")
    ax1.set_ylabel(r"$h$")
    ax1.legend()
    ax2 = fig.add_subplot(222)
    if (hm > np.max([hl, hr])):
        ax2.plot(xi, u, 'b-', label = "Physical solution")
    else:
        ax2.plot(xi, u, 'r--', label = "Unphysical solution")
    ax2.set_ybound(u_minimum - 0.1 * d_u, u_maximum + 0.1 * d_u)
    ax2.set_xlabel(r"$\xi$")
    ax2.set_ylabel(r"$u$")
    ax2.legend()

    ax3 = fig.add_subplot(223)
    left_end = np.min([-1.0, 1.1*xi1l])
    right_end = np.max([1.0, 1.1*xi2r])
    left_edge = left_end - xi1l
    right_edge = right_end - xi1r
    x1_start_points_l = np.linspace(np.min([left_edge, left_end]), 0.0, 20)
    x1_start_points_r = np.linspace(0.0, np.max([right_edge, right_end]), 20)
    t1_end_points_l = np.ones_like(x1_start_points_l)
    t1_end_points_r = np.ones_like(x1_start_points_r)

    # Look for intersections
    t1_end_points_l = np.minimum(t1_end_points_l, x1_start_points_l / (vsl - xi1l))
    x1_end_points_l = x1_start_points_l + xi1l * t1_end_points_l
    t1_end_points_r = np.minimum(t1_end_points_r, x1_start_points_r / (vsr - xi1r))
    x1_end_points_r = x1_start_points_r + xi1r * t1_end_points_r
    # Note: here we are cheating, and using the characteristic speed of the middle state,
    # ignoring how it varies across the rarefaction
    t1_final_points_r = np.ones_like(x1_start_points_r)
    t1_final_points_r = np.minimum(t1_final_points_r,
                                   (x1_end_points_r - t1_end_points_r * xi1m) / (vsl - xi1m))
    x1_final_points_r = x1_end_points_r + (t1_final_points_r - t1_end_points_r) * xi1m

    for xs, xe, te in zip(x1_start_points_l, x1_end_points_l, t1_end_points_l):
        ax3.plot([xs, xe], [0.0, te], 'b-')
    for xs, xe, te in zip(x1_start_points_r, x1_end_points_r, t1_end_points_r):
        ax3.plot([xs, xe], [0.0, te], 'g-')
    for xs, xe, ts, te in zip(x1_end_points_r, x1_final_points_r, t1_end_points_r,
                              t1_final_points_r):
        ax3.plot([xs, xe], [ts, te], 'g-')

    # Highlight the shocks
    ax3.plot([0.0, vsl], [0.0, 1.0], 'r-', linewidth=2)
    ax3.plot([0.0, vsr], [0.0, 1.0], 'r-', linewidth=2)

    # Unphysical shock
    if not physical_solution:
        x_fill = []
        if xi1l < xi1m:
            x_fill = [xi1l, 0.0, xi1m]
        elif xi1l < vsl:
            x_fill = [xi1l, 0.0, vsl]
        elif vsl < xi1m:
            x_fill = [vsl, 0.0, xi1m]
        if len(x_fill) > 0:
            t_fill = [1.0, 0.0, 1.0]
            ax3.fill_between(x_fill, t_fill, 1.0, facecolor = 'red', alpha = 0.5)

        x_fill = []
        if xi2r > xi2m:
            x_fill = [xi2m, 0.0, xi2r]
        elif xi2m < vsr:
            x_fill = [xi2m, 0.0, vsr]
        elif vsr < xi2r:
            x_fill = [vsr, 0.0, xi2r]
        if len(x_fill) > 0:
            t_fill = [1.0, 0.0, 1.0]
            ax3.fill_between(x_fill, t_fill, 1.0, facecolor = 'red', alpha = 0.5)

    ax3.set_xlabel(r"$x$")
    ax3.set_ylabel(r"$t$")
    ax3.set_title("1-characteristics")
    ax3.set_xbound(left_end, right_end)

    ax4 = fig.add_subplot(224)
    left_end = np.min([-1.0, 1.1*xi1l])
    right_end = np.max([1.0, 1.1*xi2r])
    left_edge = left_end - xi2l
    right_edge = right_end - xi2r
    x2_start_points_l = np.linspace(np.min([left_edge, left_end]), 0.0, 20)
    x2_start_points_r = np.linspace(0.0, np.max([right_edge, right_end]), 20)
    x2_end_points_r = x2_start_points_r + xi2r
    t2_end_points_l = np.ones_like(x2_start_points_l)
    t2_end_points_r = np.ones_like(x2_start_points_r)

    # Look for intersections
    t2_end_points_r = np.minimum(t2_end_points_r, x2_start_points_r / (vsr - xi2r))
    x2_end_points_r = x2_start_points_r + xi2r * t2_end_points_r
    t2_end_points_l = np.minimum(t2_end_points_l, x2_start_points_l / (vsl - xi2l))
    x2_end_points_l = x2_start_points_l + xi2l * t2_end_points_l
    # Note: here we are cheating, and using the characteristic speed of the middle state,
    # ignoring how it varies across the rarefaction
    t2_final_points_l = np.ones_like(x2_start_points_l)
    t2_final_points_l = np.minimum(t2_final_points_l,
                                   (x2_end_points_l - t2_end_points_l * xi2m) / (vsr - xi2m))
    x2_final_points_l = x2_end_points_l + (t2_final_points_l - t2_end_points_l) * xi2m

    for xs, xe, te in zip(x2_start_points_r, x2_end_points_r, t2_end_points_r):
        ax4.plot([xs, xe], [0.0, te], 'b-')
    for xs, xe, te in zip(x2_start_points_l, x2_end_points_l, t2_end_points_l):
        ax4.plot([xs, xe], [0.0, te], 'g-')
    for xs, xe, ts, te in zip(x2_end_points_l, x2_final_points_l, t2_end_points_l,
                              t2_final_points_l):
        ax4.plot([xs, xe], [ts, te], 'g-')

    # Highlight the shocks
    ax4.plot([0.0, vsl], [0.0, 1.0], 'r-', linewidth=2)
    ax4.plot([0.0, vsr], [0.0, 1.0], 'r-', linewidth=2)

    # Unphysical shock
    if not physical_solution:
        x_fill = []
        if xi1l < xi1m:
            x_fill = [xi1l, 0.0, xi1m]
        elif xi1l < vsl:
            x_fill = [xi1l, 0.0, vsl]
        elif vsl < xi1m:
            x_fill = [vsl, 0.0, xi1m]
        if len(x_fill) > 0:
            t_fill = [1.0, 0.0, 1.0]
            ax4.fill_between(x_fill, t_fill, 1.0, facecolor = 'red', alpha = 0.5)

        x_fill = []
        if xi2r > xi2m:
            x_fill = [xi2m, 0.0, xi2r]
        elif xi2m < vsr:
            x_fill = [xi2m, 0.0, vsr]
        elif vsr < xi2r:
            x_fill = [vsr, 0.0, xi2r]
        if len(x_fill) > 0:
            t_fill = [1.0, 0.0, 1.0]
            ax4.fill_between(x_fill, t_fill, 1.0, facecolor = 'red', alpha = 0.5)

    ax4.set_xlabel(r"$x$")
    ax4.set_ylabel(r"$t$")
    ax4.set_title("2-characteristics")
    ax4.set_xbound(left_end, right_end)

    fig.tight_layout()
```

```python
interactive(plot_sw_all_shock_solution,
            hl = FloatSlider(min = 0.1, max = 10.0, value = 1.0),
            ul = FloatSlider(min = -1.0, max = 1.0, value = 0.2),
            hr = FloatSlider(min = 0.1, max = 10.0, value = 1.0),
            ur = FloatSlider(min = -1.0, max = 1.0, value = -0.2))
```

Failed to display Jupyter Widget of type interactive.

  If you're reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

## Full solution

The all shock solution illustrates how the full solution can be obtained. We know that

1. the central state ${\bf w}_m$ will be connected to the known states ${\bf w}_{l, r}$ across waves that are either shocks or rarefactions,
2. if $h_m > h_{l, r}$ then the wave will be a shock, otherwise it will be a rarefaction, and
3. given $h_m$ and the known data, we can compute $u_m$ for either a shock or a rarefaction.

So, using the results above, we can find the full solution to the Riemann problem by solving the nonlinear algebraic root-finding problem
$$
  \Phi \left( h_m ; {\bf w}_l, {\bf w}_r \right) = 0,
$$
where
$$
  \Phi \left( h_m ; {\bf w}_l, {\bf w}_r \right) = \Phi_l \left( h_m ; {\bf w}_l \right) - \Phi_r \left( h_m ; {\bf w}_r \right),
$$
and
$$
\begin{align}
  \Phi_l & = u_m \left( h_m ; {\bf w}_l \right) & \Phi_r & = u_m \left( h_m ; {\bf w}_r \right) \\
  & = \begin{cases} u_l + 2 \left( \sqrt{h_l} - \sqrt{h_m} \right) & h_l > h_m \\ u_l - \sqrt{\tfrac{1}{2} \left( h_l - h_m \right) \left( \frac{h_l}{h_m} - \frac{h_m}{h_l} \right)} & h_l < h_m \end{cases} & & = \begin{cases} u_r - 2 \left( \sqrt{h_r} - \sqrt{h_m} \right) & h_r > h_m \\ u_r + \sqrt{\tfrac{1}{2} \left( h_r - h_m \right) \left( \frac{h_r}{h_m} - \frac{h_m}{h_r} \right)} & h_r < h_m \end{cases}.
\end{align}
$$

```python
def plot_sw_Riemann_curves(hl, ul, hr, ur):
    "Plot the solution curves for states (hl, ul) and (hr, ur)"

    from scipy.optimize import brentq

    def phi(hstar):
        "Function defining the root"

        if hl < hstar:
            phi_l = ul - np.sqrt(0.5 * (hl - hstar) * (hl / hstar - hstar / hl))
        else:
            phi_l = ul + 2.0 * (np.sqrt(hl) - np.sqrt(hstar))
        if hr < hstar:
            phi_r = ur + np.sqrt(0.5 * (hr - hstar) * (hr / hstar - hstar / hr))
        else:
            phi_r = ur - 2.0 * (np.sqrt(hr) - np.sqrt(hstar))

        return phi_l - phi_r

    hm = brentq(phi, 0.1 * h_min, 10.0 * h_max)
    if hl < hm:
        um = ul - np.sqrt(0.5 * (hl - hm) * (hl / hm - hm / hl))
    else:
        um = ul + 2.0 * (np.sqrt(hl) - np.sqrt(hm))

    h_maximum = np.max([h_max, hl, hr, hm])
    h_minimum = np.min([h_min, hl, hr, hm])
    u_maximum = np.max([u_max, ul, ur, um])
    u_minimum = np.min([u_min, ul, ur, um])
    dh = h_maximum - h_minimum
    du = u_maximum - u_minimum

    # Now plot the rarefaction and shock curves as appropriate
    # Here we only plot the physical pieces.

    h1_shock = np.linspace(hl, h_max)
    u1_shock = ul - np.sqrt(0.5 * (hl - h1_shock) * (hl / h1_shock - h1_shock / hl))
    h2_shock = np.linspace(hr, h_max)
    u2_shock = ur + np.sqrt(0.5 * (hr - h2_shock) * (hr / h2_shock - h2_shock / hr))

    h1_rarefaction = np.linspace(h_min, hl)
    u1_rarefaction = ul + 2.0 * (np.sqrt(hl) - np.sqrt(h1_rarefaction))
    h2_rarefaction = np.linspace(h_min, hr)
    u2_rarefaction = ur - 2.0 * (np.sqrt(hr) - np.sqrt(h2_rarefaction))

    fig = plt.figure(figsize=(12,8))
    ax = fig.add_subplot(111)
    ax.plot(hl, ul, 'rx', markersize = 16, markeredgewidth = 3, label = r"${\bf w}_l$")
    ax.plot(hr, ur, 'r+', markersize = 16, markeredgewidth = 3, label = r"${\bf w}_r$")
    ax.plot(hm, um, 'ro', markersize = 16, markeredgewidth = 3, label = r"${\bf w}_m$")
    ax.plot(h1_shock, u1_shock, 'b-', linewidth = 2,
            label="1-shock")
    ax.plot(h1_rarefaction, u1_rarefaction, 'b-.', linewidth = 2,
            label="1-rarefaction")
    ax.plot(h2_shock, u2_shock, 'g-', linewidth = 2,
            label="2-shock")
    ax.plot(h2_rarefaction, u2_rarefaction, 'g-.', linewidth = 2,
            label="2-rarefaction")
    ax.set_xlabel(r"$h$")
    ax.set_ylabel(r"$u$")
    ax.set_xbound(h_minimum - 0.1 * dh, h_maximum + 0.1 * dh)
    ax.set_ybound(u_minimum - 0.1 * du, u_maximum + 0.1 * du)
    ax.legend()

    fig.tight_layout()
```

```python
interactive(plot_sw_Riemann_curves,
            hl = FloatSlider(min = 0.1, max = 10.0, value = 1.0),
            ul = FloatSlider(min = -1.0, max = 1.0, value = 0.2),
            hr = FloatSlider(min = 0.1, max = 10.0, value = 1.0),
            ur = FloatSlider(min = -1.0, max = 1.0, value = -0.2))
```

Failed to display Jupyter Widget of type interactive.

  If you're reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

Finally, we can plot the solution in physical space.

```python
def plot_sw_Riemann_solution(hl, ul, hr, ur):
    "Plot the Riemann problem solution for states (hl, ul) and (hr, ur)"

    from scipy.optimize import brentq

    def phi(hstar):
        "Function defining the root"

        if hl < hstar:
            phi_l = ul - np.sqrt(0.5 * (hl - hstar) * (hl / hstar - hstar / hl))
        else:
            phi_l = ul + 2.0 * (np.sqrt(hl) - np.sqrt(hstar))
        if hr < hstar:
            phi_r = ur + np.sqrt(0.5 * (hr - hstar) * (hr / hstar - hstar / hr))
        else:
            phi_r = ur - 2.0 * (np.sqrt(hr) - np.sqrt(hstar))

        return phi_l - phi_r

    left_raref = False
    left_shock = False
    right_raref = False
    right_shock = False

    hm = brentq(phi, 0.1 * h_min, 10.0 * h_max)
    if hl < hm:
        um = ul - np.sqrt(0.5 * (hl - hm) * (hl / hm - hm / hl))
    else:
        um = ul + 2.0 * (np.sqrt(hl) - np.sqrt(hm))

    h_maximum = np.max([h_max, hl, hr, hm])
    h_minimum = np.min([h_min, hl, hr, hm])
    u_maximum = np.max([u_max, ul, ur, um])
    u_minimum = np.min([u_min, ul, ur, um])
    dh = h_maximum - h_minimum
    du = u_maximum - u_minimum

    xi1l = ul - np.sqrt(hl)
    xi1m = um - np.sqrt(hm)
    xi1r = ur - np.sqrt(hr)
    if hm > hl:
        left_shock = True
        vsl = ul - hm / (hm - hl) * np.sqrt(0.5 * (hl - hm) * (hl / hm - hm / hl))
    else:
        left_raref = True
        hl_raref = np.linspace(hl, hm, 20)
        ul_raref = ul + 2.0 * (np.sqrt(hl) - np.sqrt(hl_raref))
        xil_raref = ul_raref - np.sqrt(hl_raref)

    xi2r = ur + np.sqrt(hr)
    xi2m = um + np.sqrt(hm)
    xi2l = ul + np.sqrt(hl)
    if hm > hr:
        right_shock = True
        vsr = ur + hm / (hm - hr) * np.sqrt(0.5 * (hr - hm) * (hr / hm - hm / hr))
    else:
        right_raref = True
        hr_raref = np.linspace(hm, hr)
        ur_raref = ur - 2.0 * (np.sqrt(hr) - np.sqrt(hr_raref))
        xir_raref = ur_raref + np.sqrt(hr_raref)

    xi_min = np.min([-1.0, xi1l, xi1m, xi2r, xi2m])
    xi_max = np.max([1.0, xi1l, xi1m, xi2r, xi2m])
    d_xi = xi_max - xi_min
    h_maximum = np.max([hl, hr, hm])
    h_minimum = np.min([hl, hr, hm])
    d_h = h_maximum - h_minimum
    u_maximum = np.max([ul, ur, um])
    u_minimum = np.min([ul, ur, um])
    d_u = u_maximum - u_minimum

    xi = np.array([xi_min - 0.1 * d_xi])
    h = np.array([hl])
    u = np.array([ul])
    if left_shock:
        xi = np.append(xi, [vsl, vsl])
        h = np.append(h, [hl, hm])
        u = np.append(u, [ul, um])
    else:
        xi = np.append(xi, xil_raref)
        h = np.append(h, hl_raref)
        u = np.append(u, ul_raref)
    if right_shock:
        xi = np.append(xi, [vsr, vsr])
        h = np.append(h, [hm, hr])
        u = np.append(u, [um, ur])
    else:
        xi = np.append(xi, xir_raref)
        h = np.append(h, hr_raref)
        u = np.append(u, ur_raref)
    xi = np.append(xi, [xi_max + 0.1 * d_xi])
    h = np.append(h, [hr])
    u = np.append(u, [ur])

    fig = plt.figure(figsize=(12,8))
    ax1 = fig.add_subplot(221)
    ax1.plot(xi, h, 'b-', label = "True solution")
    ax1.set_ybound(h_minimum - 0.1 * d_h, h_maximum + 0.1 * d_h)
    ax1.set_xlabel(r"$\xi$")
    ax1.set_ylabel(r"$h$")
    ax1.legend()
    ax2 = fig.add_subplot(222)
    ax2.plot(xi, u, 'b-', label = "True solution")
    ax2.set_ybound(u_minimum - 0.1 * d_u, u_maximum + 0.1 * d_u)
    ax2.set_xlabel(r"$\xi$")
    ax2.set_ylabel(r"$u$")
    ax2.legend()

    ax3 = fig.add_subplot(223)
    left_end = np.min([-1.0, 1.1*xi1l])
    right_end = np.max([1.0, 1.1*xi2r])
    left_edge = left_end - xi1l
    right_edge = right_end - xi1r
    x1_start_points_l = np.linspace(np.min([left_edge, left_end]), 0.0, 20)
    x1_start_points_r = np.linspace(0.0, np.max([right_edge, right_end]), 20)
    t1_end_points_l = np.ones_like(x1_start_points_l)
    t1_end_points_r = np.ones_like(x1_start_points_r)

    # Look for intersections
    if left_shock:
        t1_end_points_l = np.minimum(t1_end_points_l, x1_start_points_l / (vsl - xi1l))
    x1_end_points_l = x1_start_points_l + xi1l * t1_end_points_l
    if right_shock:
        t1_end_points_r = np.minimum(t1_end_points_r, x1_start_points_r / (vsr - xi1r))
    else:
        t1_end_points_r = np.minimum(t1_end_points_r, x1_start_points_r / (xi2r - xi1r))
    x1_end_points_r = x1_start_points_r + xi1r * t1_end_points_r
    # Note: here we are cheating, and using the characteristic speed of the middle state,
    # ignoring how it varies across the rarefaction
    t1_final_points_r = np.ones_like(x1_start_points_r)
    if left_shock:
        t1_final_points_r = np.minimum(t1_final_points_r,
                                       (x1_end_points_r - t1_end_points_r * xi1m) /
                                       (vsl - xi1m))
    x1_final_points_r = x1_end_points_r + (t1_final_points_r - t1_end_points_r) * xi1m

    for xs, xe, te in zip(x1_start_points_l, x1_end_points_l, t1_end_points_l):
        ax3.plot([xs, xe], [0.0, te], 'b-')
    for xs, xe, te in zip(x1_start_points_r, x1_end_points_r, t1_end_points_r):
        ax3.plot([xs, xe], [0.0, te], 'g-')
    for xs, xe, ts, te in zip(x1_end_points_r, x1_final_points_r, t1_end_points_r,
                              t1_final_points_r):
        ax3.plot([xs, xe], [ts, te], 'g-')

    # Highlight the waves
    if left_shock:
        ax3.plot([0.0, vsl], [0.0, 1.0], 'r-', linewidth=2)
    else:
        ax3.plot([0.0, xi1l], [0.0, 1.0], 'r-', linewidth=2)
        ax3.plot([0.0, xi1m], [0.0, 1.0], 'r-', linewidth=2)
        xi = np.linspace(xi1l, xi1m, 11)
        x_end_rarefaction = xi
        for xe in x_end_rarefaction:
            ax3.plot([0.0, xe], [0.0, 1.0], 'r--')
    if right_shock:
        ax3.plot([0.0, vsr], [0.0, 1.0], 'r-', linewidth=2)
    else:
        ax3.plot([0.0, xi2m], [0.0, 1.0], 'r-', linewidth=2)
        ax3.plot([0.0, xi2r], [0.0, 1.0], 'r-', linewidth=2)

    ax3.set_xlabel(r"$x$")
    ax3.set_ylabel(r"$t$")
    ax3.set_title("1-characteristics")
    ax3.set_xbound(left_end, right_end)

    ax4 = fig.add_subplot(224)
    left_end = np.min([-1.0, 1.1*xi1l])
    right_end = np.max([1.0, 1.1*xi2r])
    left_edge = left_end - xi2l
    right_edge = right_end - xi2r
    x2_start_points_l = np.linspace(np.min([left_edge, left_end]), 0.0, 20)
    x2_start_points_r = np.linspace(0.0, np.max([right_edge, right_end]), 20)
    x2_end_points_r = x2_start_points_r + xi2r
    t2_end_points_l = np.ones_like(x2_start_points_l)
    t2_end_points_r = np.ones_like(x2_start_points_r)

    # Look for intersections
    if right_shock:
        t2_end_points_r = np.minimum(t2_end_points_r, x2_start_points_r / (vsr - xi2r))
    x2_end_points_r = x2_start_points_r + xi2r * t2_end_points_r
    if left_shock:
        t2_end_points_l = np.minimum(t2_end_points_l, x2_start_points_l / (vsl - xi2l))
    else:
        t2_end_points_l = np.minimum(t2_end_points_l, x2_start_points_l / (xi1l - xi2l))
    x2_end_points_l = x2_start_points_l + xi2l * t2_end_points_l
    # Note: here we are cheating, and using the characteristic speed of the middle state,
    # ignoring how it varies across the rarefaction
    t2_final_points_l = np.ones_like(x2_start_points_l)
    if right_shock:
        t2_final_points_l = np.minimum(t2_final_points_l,
                                       (x2_end_points_l - t2_end_points_l * xi2m) /
                                       (vsr - xi2m))
    x2_final_points_l = x2_end_points_l + (t2_final_points_l - t2_end_points_l) * xi2m

    for xs, xe, te in zip(x2_start_points_r, x2_end_points_r, t2_end_points_r):
        ax4.plot([xs, xe], [0.0, te], 'b-')
    for xs, xe, te in zip(x2_start_points_l, x2_end_points_l, t2_end_points_l):
        ax4.plot([xs, xe], [0.0, te], 'g-')
    for xs, xe, ts, te in zip(x2_end_points_l, x2_final_points_l, t2_end_points_l,
                              t2_final_points_l):
        ax4.plot([xs, xe], [ts, te], 'g-')

    # Highlight the waves
    if left_shock:
        ax4.plot([0.0, vsl], [0.0, 1.0], 'r-', linewidth=2)
    else:
        ax4.plot([0.0, xi1l], [0.0, 1.0], 'r-', linewidth=2)
        ax4.plot([0.0, xi1m], [0.0, 1.0], 'r-', linewidth=2)
    if right_shock:
        ax4.plot([0.0, vsr], [0.0, 1.0], 'r-', linewidth=2)
    else:
        ax4.plot([0.0, xi2m], [0.0, 1.0], 'r-', linewidth=2)
        ax4.plot([0.0, xi2r], [0.0, 1.0], 'r-', linewidth=2)
        xi = np.linspace(xi2m, xi2r, 11)
        x_end_rarefaction = xi
        for xe in x_end_rarefaction:
            ax4.plot([0.0, xe], [0.0, 1.0], 'r--')

    ax4.set_xlabel(r"$x$")
    ax4.set_ylabel(r"$t$")
    ax4.set_title("2-characteristics")
    ax4.set_xbound(left_end, right_end)

    fig.tight_layout()
```

```python
interactive(plot_sw_Riemann_solution,
            hl = FloatSlider(min = 0.1, max = 10.0, value = 1.0),
            ul = FloatSlider(min = -1.0, max = 1.0, value = 0.2),
            hr = FloatSlider(min = 0.1, max = 10.0, value = 1.0),
            ur = FloatSlider(min = -1.0, max = 1.0, value = -0.2))
```

Failed to display Jupyter Widget of type interactive.

  If you're reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.


```python
# Erasmus+ ICCT project (2018-1-SI01-KA203-047081)

# Toggle cell visibility

from IPython.display import HTML
tag = HTML('''
Toggle cell visibility <a href="javascript:code_toggle()">here</a>.''')
display(tag)

# Hide the code completely

# from IPython.display import HTML
# tag = HTML('''<style>
# div.input {
#     display:none;
# }
# </style>''')
# display(tag)
```

Toggle cell visibility here.

## Krmiljenje povratne zveze stanj - zmogljivost krmiljenja

Za sistem:

$$
\dot{x}=\underbrace{\begin{bmatrix}-0.5&1\\0&-0.1\end{bmatrix}}_{A}x+\underbrace{\begin{bmatrix}0\\1\end{bmatrix}}_{B}u
$$

načrtuj krmilnik tako, da bo prva spremenljivka stanja sistema sledila referenčni koračni funkciji brez odstopka v stacionarnem času s časom ustalitve (odziv naj doseže 95% končne vrednosti) krajšim od 1 s.

Z namenom zagotovitve zgornjim zahtevam dodamo fiktivno spremenljivko stanja $x_3$ z dinamiko $\dot{x_3}=x_1-x_{1r}$, kjer $x_{1r}$ predstavlja referenčni signal, tako da, če je razširjen sistem asimptotično stabilen, potem konvergira nova spremenljivka stanja $x_3$ k vrednosti 0, kar zagotavlja, da gre $x_1$ k vrednosti $x_{1r}$.

Razširjen sistem lahko popišemo z naslednjimi enačbami:

$$
\dot{x}_a=\underbrace{\begin{bmatrix}-0.5&1&0\\0&-0.1&0\\1&0&0\end{bmatrix}}_{A_a}x_a+\underbrace{\begin{bmatrix}0\\1\\0\end{bmatrix}}_{B_a}u+\underbrace{\begin{bmatrix}0\\0\\-1\end{bmatrix}}_{B_{\text{ref}}}x_{1r}
$$

in naslednjo spoznavnostno matriko:

$$
\begin{bmatrix}B_a&A_aB_a&A_a^2B_a\end{bmatrix} = \begin{bmatrix}0&1&-0.6\\1&-0.1&0.01\\0&0&1\end{bmatrix}
$$

Ker $\text{rank}=3$  je razširjen sistem vodljiv.

Z namenom zagotovitve druge zahteve, je možna rešitev ta, da s prilagajanjem polov dosežemo, da ima sistem dominanten pol pri $-3$ rad/s (opomba: $e^{\lambda t}=e^{-3t}$ pri $t=1$ s znaša $0.4978..<0.05$). Izbrana pola sta tako $\lambda_1=-3\,\text{in}\,\lambda_2=\lambda_3=-30$, s pripadajočo matriko ojačanja $K_a=\begin{bmatrix}1048.75&62.4&2700\end{bmatrix}$.

Zaprtozančni sistem lahko tako zapišemo kot:

$$
\dot{x}_a=(A_a-B_aK_a)x_a+B_av+B_{\text{ref}}x_{1r}=\begin{bmatrix}-0.5&1&0\\-1048.75&-62.5&-2700\\1&0&0\end{bmatrix}x_a+\begin{bmatrix}0\\1\\0\end{bmatrix}v+\begin{bmatrix}0\\0\\-1\end{bmatrix}x_{1r}
$$

### Kako upravljati s tem interaktivnim primerom?
Preizkusi različne rešitve s spreminjanjem ojačanja $K$ ali neposrednim določanjem vrednosti zaprtozančnih lastnih vrednosti.

```python
%matplotlib inline
import control as control
import numpy
import sympy as sym
from IPython.display import display, Markdown
import ipywidgets as widgets
import matplotlib.pyplot as plt

#print a matrix latex-like
def bmatrix(a):
     """Returns a LaTeX bmatrix - by Damir Arbula (ICCT project)

     :a: numpy array
     :returns: LaTeX bmatrix as a string
     """
     if len(a.shape) > 2:
         raise ValueError('bmatrix can at most display two dimensions')
     lines = str(a).replace('[', '').replace(']', '').splitlines()
     rv = [r'\begin{bmatrix}']
     rv += ['  ' + ' & '.join(l.split()) + r'\\' for l in lines]
     rv +=  [r'\end{bmatrix}']
     return '\n'.join(rv)

# Display formatted matrix:
def vmatrix(a):
    if len(a.shape) > 2:
         raise ValueError('bmatrix can at most display two dimensions')
    lines = str(a).replace('[', '').replace(']', '').splitlines()
    rv = [r'\begin{vmatrix}']
    rv += ['  ' + ' & '.join(l.split()) + r'\\' for l in lines]
    rv +=  [r'\end{vmatrix}']
    return '\n'.join(rv)

#matrixWidget is a matrix looking widget built with a VBox of HBox(es) that returns a numPy array as value !
class matrixWidget(widgets.VBox):
    def updateM(self,change):
        for irow in range(0,self.n):
            for icol in range(0,self.m):
                self.M_[irow,icol] = self.children[irow].children[icol].value
                #print(self.M_[irow,icol])
        self.value = self.M_

    def dummychangecallback(self,change):
        pass

    def __init__(self,n,m):
        self.n = n
        self.m = m
        self.M_ = numpy.matrix(numpy.zeros((self.n,self.m)))
        self.value = self.M_
        widgets.VBox.__init__(self,
                             children = [
                                 widgets.HBox(children =
                                              [widgets.FloatText(value=0.0, layout=widgets.Layout(width='90px')) for i in range(m)]
                                             )
                                 for j in range(n)
                             ])

        #fill in widgets and tell interact to call updateM each time a children changes value
        for irow in range(0,self.n):
            for icol in range(0,self.m):
                self.children[irow].children[icol].value = self.M_[irow,icol]
                self.children[irow].children[icol].observe(self.updateM, names='value')
        #value = Unicode('example@example.com', help="The email value.").tag(sync=True)
        self.observe(self.updateM, names='value', type= 'All')

    def setM(self, newM):
        #disable callbacks, change values, and reenable
        self.unobserve(self.updateM, names='value', type= 'All')
        for irow in range(0,self.n):
            for icol in range(0,self.m):
                self.children[irow].children[icol].unobserve(self.updateM, names='value')
        self.M_ = newM
        self.value = self.M_
        for irow in range(0,self.n):
            for icol in range(0,self.m):
                self.children[irow].children[icol].value = self.M_[irow,icol]
        for irow in range(0,self.n):
            for icol in range(0,self.m):
                self.children[irow].children[icol].observe(self.updateM, names='value')
        self.observe(self.updateM, names='value', type= 'All')

                #self.children[irow].children[icol].observe(self.updateM, names='value')

#overlaod class for state space systems that DO NOT remove "useless" states (what "professor" of automatic control would do this?)
class sss(control.StateSpace):
    def __init__(self,*args):
        #call base class init constructor
        control.StateSpace.__init__(self,*args)
    #disable function below in base class
    def _remove_useless_states(self):
        pass
```

```python
# Preparatory cell

A = numpy.matrix('-0.5 1 0; 0 -0.1 0; 1 0 0')
B = numpy.matrix('0; 1; 0')
Br = numpy.matrix('0; 0; -1')
C = numpy.matrix('1 0 0')
X0 = numpy.matrix('0; 0; 0')
K = numpy.matrix([1048.75,62.4,2700])

Aw = matrixWidget(3,3)
Aw.setM(A)
Bw = matrixWidget(3,1)
Bw.setM(B)
Brw = matrixWidget(3,1)
Brw.setM(Br)
Cw = matrixWidget(1,3)
Cw.setM(C)
X0w = matrixWidget(3,1)
X0w.setM(X0)
Kw = matrixWidget(1,3)
Kw.setM(K)

eig1c = matrixWidget(1,1)
eig2c = matrixWidget(2,1)
eig3c = matrixWidget(1,1)
eig1c.setM(numpy.matrix([-3]))
eig2c.setM(numpy.matrix([[-30],[0]]))
eig3c.setM(numpy.matrix([-30]))
```

```python
# Misc

#create dummy widget
DW = widgets.FloatText(layout=widgets.Layout(width='0px', height='0px'))

#create button widget
START = widgets.Button(
    description='Test',
    disabled=False,
    button_style='', # 'success', 'info', 'warning', 'danger' or ''
    tooltip='Test',
    icon='check'
)

def on_start_button_clicked(b):
    #This is a workaround to have intreactive_output call the callback:
    #   force the value of the dummy widget to change
    if DW.value> 0 :
        DW.value = -1
    else:
        DW.value = 1
    pass
START.on_click(on_start_button_clicked)

# Define type of method
selm = widgets.Dropdown(
    options= ['Nastavi K', 'Nastavi lastne vrednosti'],
    value= 'Nastavi K',
    description='',
    disabled=False
)

# Define the number of complex eigenvalues for the observer
selc = widgets.Dropdown(
    options= ['brez kompleksnih lastnih vrednosti', 'dve kompleksni lastni vrednosti'],
    value= 'brez kompleksnih lastnih vrednosti',
    description='Lastne vrednosti:',
    disabled=False
)

#define type of ipout
selu = widgets.Dropdown(
    options=['impulzna funkcija', 'koračna funkcija', 'sinusoidna funkcija', 'kvadratni val'],
    value='impulzna funkcija',
    description='Vhod:',
    disabled=False,
    style = {'description_width': 'initial','button_width':'180px'}
)
# Define the values of the input
u = widgets.FloatSlider(
    value=1,
    min=0,
    max=20.0,
    step=0.1,
    description='Referenca:',
    disabled=False,
    continuous_update=False,
    orientation='horizontal',
    readout=True,
    readout_format='.1f',
)
period = widgets.FloatSlider(
    value=0.5,
    min=0.01,
    max=4,
    step=0.01,
    description='Perioda: ',
    disabled=False,
    continuous_update=False,
    orientation='horizontal',
    readout=True,
    readout_format='.2f',
)
```

```python
# Support functions

def eigen_choice(selc):
    if selc == 'brez kompleksnih lastnih vrednosti':
        eig1c.children[0].children[0].disabled = False
        eig2c.children[1].children[0].disabled = True
        eigc = 0
    if selc == 'dve kompleksni lastni vrednosti':
        eig1c.children[0].children[0].disabled = True
        eig2c.children[1].children[0].disabled = False
        eigc = 2
    return eigc

def method_choice(selm):
    if selm == 'Nastavi K':
        method = 1
        selc.disabled = True
    if selm == 'Nastavi lastne vrednosti':
        method = 2
        selc.disabled = False
    return method
```

```python
def main_callback(Aw, Bw, Brw, X0w, K, eig1c, eig2c, eig3c, u, period, selm, selc, selu, DW):
    A, B, Br = Aw, Bw, Brw
    sols = numpy.linalg.eig(A)
    eigc = eigen_choice(selc)
    method = method_choice(selm)

    if method == 1:
        sol = numpy.linalg.eig(A-B*K)
    if method == 2:
        if eigc == 0:
            K = control.acker(A, B, [eig1c[0,0], eig2c[0,0], eig3c[0,0]])
            Kw.setM(K)
        if eigc == 2:
            K = control.acker(A, B, [eig1c[0,0],
                                      numpy.complex(eig2c[0,0],eig2c[1,0]),
                                      numpy.complex(eig2c[0,0],-eig2c[1,0])])
            Kw.setM(K)
        sol = numpy.linalg.eig(A-B*K)
    print('Lastne vrednosti sistema so:',round(sols[0][0],4),',',round(sols[0][1],4),'in',round(sols[0][2],4))
    print('Lastne vrednosti krmiljenega sistema so:',round(sol[0][0],4),',',round(sol[0][1],4),'in',round(sol[0][2],4))

    sys = sss(A-B*K,Br,C,0)
    T = numpy.linspace(0, 6, 1000)

    if selu == 'impulzna funkcija': #selu
        U = [0 for t in range(0,len(T))]
        U[0] = u
        T, yout, xout = control.forced_response(sys,T,U,X0w)
    if selu == 'koračna funkcija':
        U = [u for t in range(0,len(T))]
        T, yout, xout = control.forced_response(sys,T,U,X0w)
    if selu == 'sinusoidna funkcija':
        U = u*numpy.sin(2*numpy.pi/period*T)
        T, yout, xout = control.forced_response(sys,T,U,X0w)
    if selu == 'kvadratni val':
        U = u*numpy.sign(numpy.sin(2*numpy.pi/period*T))
        T, yout, xout = control.forced_response(sys,T,U,X0w)

    fig = plt.figure(num='Simulacija', figsize=(16,10))

    fig.add_subplot(211)
    plt.title('Odziv prve spremenljivke stanj')
    plt.ylabel('$X_1$ vs ref')
    plt.plot(T,xout[0],T,U,'r--')
    plt.xlabel('$t$ [s]')
    plt.legend(['$x_1$','Referenca'])
    plt.axvline(x=0,color='black',linewidth=0.8)
    plt.axhline(y=0,color='black',linewidth=0.8)
    plt.grid()

    fig.add_subplot(212)
    poles, zeros = control.pzmap(sys,Plot=False)
    plt.title('Diagram polov in ničel')
    plt.ylabel('Im')
    plt.plot(numpy.real(poles),numpy.imag(poles),'rx',numpy.real(zeros),numpy.imag(zeros),'bo')
    plt.xlabel('Re')
    plt.axvline(x=0,color='black',linewidth=0.8)
    plt.axhline(y=0,color='black',linewidth=0.8)
    plt.grid()

alltogether = widgets.VBox([widgets.HBox([selm,
                                          selc,
                                          selu]),
                            widgets.Label(' ',border=3),
                            widgets.HBox([widgets.Label('K:',border=3), Kw,
                                          widgets.Label(' ',border=3),
                                          widgets.Label(' ',border=3),
                                          widgets.Label('Lastne vrednosti:',border=3),
                                          eig1c,
                                          eig2c,
                                          eig3c,
                                          widgets.Label(' ',border=3),
                                          widgets.Label(' ',border=3),
                                          widgets.Label('X0:',border=3), X0w]),
                            widgets.Label(' ',border=3),
                            widgets.HBox([u,
                                          period,
                                          START]),
                            widgets.Label(' ',border=3),
                            widgets.HBox([widgets.Label('Dinamična matrika Aa:',border=3),
                                          Aw,
                                          widgets.Label('Vhodna matrika Ba:',border=3),
                                          Bw,
                                          widgets.Label('Referenčna matrika Br:',border=3),
                                          Brw])])
out = widgets.interactive_output(main_callback, {'Aw':Aw, 'Bw':Bw, 'Brw':Brw, 'X0w':X0w, 'K':Kw, 'eig1c':eig1c, 'eig2c':eig2c, 'eig3c':eig3c,
                                                 'u':u, 'period':period, 'selm':selm, 'selc':selc, 'selu':selu, 'DW':DW})
out.layout.height = '640px'
display(out, alltogether)
```

    Output(layout=Layout(height='640px'))

    VBox(children=(HBox(children=(Dropdown(options=('Nastavi K', 'Nastavi lastne vrednosti'), value='Nastavi K'), …

```python

```


# Optimización media-varianza

La **teoría de portafolios** es una de los avances más importantes en las finanzas modernas e inversiones.
- Apareció por primera vez en un [artículo corto](https://www.math.ust.hk/~maykwok/courses/ma362/07F/markowitz_JF.pdf) llamado "Portfolio Selection" en la edición de Marzo de 1952 de "the Journal of Finance".
- Escrito por un desconocido estudiante de la Universidad de Chicago, llamado Harry Markowitz.
- Escrito corto (sólo 14 páginas), poco texto, fácil de entender, muchas gráficas y unas cuantas referencias.
- No se le prestó mucha atención hasta los 60s.

Finalmente, este trabajo se convirtió en una de las más grandes ideas en finanzas, y le dió a Markowitz el Premio Noble casi 40 años después.
- Markowitz estaba incidentalmente interesado en los mercados de acciones e inversiones.
- Estaba más bien interesado en entender cómo las personas tomaban sus mejores decisiones cuando se enfrentaban con "trade-offs".
- Principio de conservación de la miseria. O, dirían los instructores de gimnasio: "no pain, no gain".
- Si queremos más de algo, tenemos que perder en algún otro lado.
- El estudio de este fenómeno era el que le atraía a Markowitz.

De manera que nadie se hace rico poniendo todo su dinero en la cuenta de ahorros. La única manera de esperar altos rendimientos es si se toma bastante riesgo. Sin embargo, riesgo significa también la posibilidad de perder, tanto como ganar.

Pero, ¿qué tanto riesgo es necesario?, y ¿hay alguna manera de minimizar el riesgo mientras se maximizan las ganancias?
- Markowitz básicamente cambió la manera en que los inversionistas pensamos acerca de esas preguntas.
- Alteró completamente la práctica de la administración de inversiones.
- Incluso el título de su artículo era innovador. Portafolio: una colección de activos en lugar de tener activos individuales.
- En ese tiempo, un portafolio se refería a una carpeta de cuero.
- En el resto de este módulo, no ocuparemos de la parte analítica de la teoría de portafolios, la cual puede ser resumida en dos frases:
 - No pain, no gain.
 - No ponga todo el blanquillo en una sola bolsa.

**Objetivos:**
- ¿Qué es la línea de asignación de capital?
- ¿Qué es el radio de Sharpe?
- ¿Cómo deberíamos asignar nuestro capital entre un activo riesgoso y un activo libre de riesgo?

*Referencia:*
- Notas del curso "Portfolio Selection and Risk Management", Rice University, disponible en Coursera.
___

## 1. Línea de asignación de capital

### 1.1. Motivación

El proceso de construcción de un portafolio tiene entonces los siguientes dos pasos:
1. Escoger un portafolio de activos riesgosos.
2. Decidir qué tanto de tu riqueza invertirás en el portafolio y qué tanto invertirás en activos libres de riesgo.

Al paso 2 lo llamamos **decisión de asignación de activos**.

Preguntas importantes:
1. ¿Qué es el portafolio óptimo de activos riesgosos?
 - ¿Cuál es el mejor portafolio de activos riesgosos?
 - Es un portafolio eficiente en media-varianza.
2. ¿Qué es la distribución óptima de activos?
 - ¿Cómo deberíamos distribuir nuestra riqueza entre el portafolo riesgoso óptimo y el activo libre de riesgo?
 - Concepto de **línea de asignación de capital**.
 - Concepto de **radio de Sharpe**.

Dos suposiciones importantes:
- Funciones de utilidad media-varianza.
- Inversionista averso al riesgo.

La idea sorprendente que saldrá de este análisis, es que cualquiera que sea la actitud del inversionista de cara al riesgo, el mejor portafolio de activos riesgosos es idéntico para todos los inversionistas.

Lo que nos importará a cada uno de nosotros en particular, es simplemente la desición óptima de asignación de activos.
___

### 1.2. Línea de asignación de capital

Sean:
- $r_s$ el rendimiento del activo riesgoso,
- $r_f$ el rendimiento libre de riesgo, y
- $w$ la fracción invertida en el activo riesgoso.

 Realizar deducción de la línea de asignación de capital en el tablero.

**Tres doritos después...**

#### Línea de asignación de capital (LAC):
$E[r_p]$ se relaciona con $\sigma_p$ de manera afín. Es decir, mediante la ecuación de una recta:

$$E[r_p]=r_f+\frac{E[r_s-r_f]}{\sigma_s}\sigma_p.$$

- La pendiente de la LAC es el radio de Sharpe $\frac{E[r_s-r_f]}{\sigma_s}=\frac{E[r_s]-r_f}{\sigma_s}$,
- el cual nos dice qué tanto rendimiento obtenemos por unidad de riesgo asumido en la tenencia del activo (portafolio) riesgoso.

Ahora, la pregunta es, ¿dónde sobre esta línea queremos estar?
___

### 1.3. Resolviendo para la asignación óptima de capital

Recapitulando de la clase pasada, tenemos las curvas de indiferencia: **queremos estar en la curva de indiferencia más alta posible, que sea tangente a la LAC**.

 Ver en el tablero.

Analíticamente, el problema es

$$\max_{w} \quad E[U(r_p)]\equiv\max_{w} \quad E[r_p]-\frac{1}{2}\gamma\sigma_p^2,$$

donde los puntos $(\sigma_p,E[r_p])$ se restringen a estar en la LAC, esto es $E[r_p]=r_f+\frac{E[r_s-r_f]}{\sigma_s}\sigma_p$ y $\sigma_p=w\sigma_s$. Entonces el problema anterior se puede escribir de la siguiente manera:

$$\max_{w} \quad r_f+wE[r_s-r_f]-\frac{1}{2}\gamma w^2\sigma_s^2.$$

 Encontrar la $w$ que maximiza la anterior expresión en el tablero.

**Tres doritos después...**

La solución es entonces:

$$w^\ast=\frac{E[r_s-r_f]}{\gamma\sigma_s^2}.$$

De manera intuitiva:
- $w^\ast\propto E[r_s-r_f]$: a más exceso de rendimiento que se obtenga del activo riesgoso, más querremos invertir en él.
- $w^\ast\propto \frac{1}{\gamma}$: mientras más averso al riesgo seas, menos querrás invertir en el activo riesgoso.
- $w^\ast\propto \frac{1}{\sigma_s^2}$: mientras más riesgoso sea el activo, menos querrás invertir en él.
___

## 2. Ejemplo de asignación óptima de capital: acciones y billetes de EU

Pongamos algunos números con algunos datos, para ilustrar la derivación que acabamos de hacer.

En este caso, consideraremos:
- **Portafolio riesgoso**: mercado de acciones de EU (representados en algún índice de mercado como el S&P500).
- **Activo libre de riesgo**: billetes del departamento de tesorería de EU (T-bills).

Tenemos los siguientes datos:

$$E[r_{US}]=11.9\%,\quad \sigma_{US}=19.15\%, \quad r_f=1\%.$$

Recordamos que podemos escribir la expresión de la LAC como:

\begin{align}
E[r_p]&=r_f+\left[\frac{E[r_{US}-r_f]}{\sigma_{US}}\right]\sigma_p\\
      &=0.01+\text{S.R.}\sigma_p,
\end{align}

donde $\text{S.R}=\frac{0.119-0.01}{0.1915}\approx0.569$ es el radio de Sharpe (¿qué es lo que es esto?).

Grafiquemos la LAC con estos datos reales:

```python
# Importamos librerías que vamos a utilizar
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
```

```python
# Datos
Erus, sus, rf = 0.119, 0.1915, 0.01
# Radio de Sharpe para este activo
SR = (Erus-rf)/sus
# Vector de volatilidades del portafolio
sp = np.linspace(0, 0.5, 100)
# LAC
Erp = rf+SR*sp
```

```python
# Gráfica
plt.figure(figsize=(10,6))
plt.plot(sp, Erp, lw='3', label='LAC')
plt.plot(0, rf, 'o', ms=10, label='Libre de riesgo')
plt.plot(sus, Erus, 'o', ms=10, label='Portafolio riesgoso')
plt.axhline(y=Erus, color='gray')
plt.axvline(x=sus, color='gray')
plt.axhline(y=0, color='k')
plt.axvline(x=0, color='k')
plt.grid()
plt.xlabel('Volatility $\sigma_p$')
plt.ylabel('Expected return $E[r_p]$')
plt.legend(loc='best')
```

Bueno, y ¿en qué punto de esta línea querríamos estar?
- Pues ya vimos que depende de tus preferencias.
- En particular, de tu actitud de cara al riesgo, medido por tu coeficiente de aversión al riesgo.

Solución al problema de asignación óptima de capital:

$$\max_{w} \quad E[U(r_p)]$$

$$w^\ast=\frac{E[r_s-r_f]}{\gamma\sigma_s^2}$$

Dado que ya tenemos datos, podemos intentar para varios coeficientes de aversión al riesgo:

```python
# importar pandas
import pandas as pd
```

```python
# Crear un DataFrame con los pesos, rendimiento
# esperado y volatilidad del portafolio óptimo
# entre los activos riesgoso y libre de riesgo
# cuyo índice sean los coeficientes de aversión
# al riesgo del 1 al 10 (enteros)
g = np.arange(1, 11)
wopt = (Erus-rf)/(g*sus**2)
sp = wopt*sus
Erp = rf+(Erus-rf)/sus*sp
data = pd.DataFrame(index=g, columns=['$w_{opt}$', '$E[r_p]$', '$\sigma_p$'])
data.index.name = '$\gamma$'
data['$w_{opt}$'] = wopt
data['$E[r_p]$'] = Erp
data['$\sigma_p$'] = sp
data
```

      $w_{opt}$
      $E[r_p]$
      $\sigma_p$

      $\gamma$

      1
      2.972275
      0.333978
      0.569191

      2
      1.486137
      0.171989
      0.284595

      3
      0.990758
      0.117993
      0.189730

      4
      0.743069
      0.090994
      0.142298

      5
      0.594455
      0.074796
      0.113838

      6
      0.495379
      0.063996
      0.094865

      7
      0.424611
      0.056283
      0.081313

      8
      0.371534
      0.050497
      0.071149

      9
      0.330253
      0.045998
      0.063243

      10
      0.297227
      0.042398
      0.056919

¿Cómo se interpreta $w^\ast>1$?
- Cuando $0<w^\ast<1$, entonces $0<1-w^\ast<1$. Lo cual implica posiciones largas en el mercado de activos y en el activo libre de riesgo.
- Por el contrario, cuando $w^\ast>1$, tenemos $1-w^\ast<0$. Lo anterior implica una posición corta en el activo libre de riesgo (suponiendo que se puede) y una posición larga (de más del 100%) en el mercado de activos: apalancamiento.

# Anuncios parroquiales.

## 1. Quiz la siguiente clase.
## 2. [Calificaciones](https://docs.google.com/spreadsheets/d/18-SDXpkuN6LULO16_1VPHPksrP-QCEpj7OLxiaSpQ3U/edit?usp=sharing)

Created with Jupyter by Esteban Jiménez Rodríguez.


```python
from IPython.core.display import HTML, Image
css_file = 'style.css'
HTML(open(css_file, 'r').read())
```

<link href='http://fonts.googleapis.com/css?family=Alegreya+Sans:100,300,400,500,700,800,900,100italic,300italic,400italic,500italic,700italic,800italic,900italic' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Arvo:400,700,400italic' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=PT+Mono' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Shadows+Into+Light' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Philosopher:400,700,400italic,700italic' rel='stylesheet' type='text/css'>

```python
from sympy import init_printing, Matrix, symbols
from IPython.display import Image
from warnings import filterwarnings
```

```python
init_printing(use_latex = 'mathjax')
filterwarnings('ignore')
```

# Projections onto subspaces

## Geometry in the plane

* Projection of a vector onto another (in the plane)
* Consider the orthogonal projection of **b** onto **a**

```python
Image(filename = 'Orthogonal projection in the plane.png')
```

* Note that **p** falls on a line, which is a subspace of the plane ℝ2
* Remember from the previous lecture that orthogonal subspaces have A**x** = **0**
* Note that **p** is some scalar multiple of **a**
* With **a** perpendicular to **e** and **e** = **b** - x**a**
* Thus we have **a**T(**b** - x**a**) = 0 and x**a**T**a** = **a**T**b**
* Since **a**T**a** is a number we can simplify
$$ x=\frac { { \underline { a }  }^{ T }\underline { b }  }{ { \underline { a }  }^{ T }\underline { a }  }  $$

* We also have **p** = **a**x
$$ \underline { p } =\underline { a } x=\underline { a } \frac { { \underline { a }  }^{ T }\underline { b }  }{ { \underline { a }  }^{ T }\underline { a }  }  $$

* This equation is helpful
    * Doubling (or any other scalar multiple of) **b** doubles (or scalar multiplies) **p**
    * Doubling (or scalar multiple of) **a** has no effect

* Eventually we are looking for proj**p** = P**b**, where P is the projection matrix
$$ \underline { p } =P\underline { b } \\ P=\frac { 1 }{ { \underline { a }  }^{ T }\underline { a }  } \underline { a } { \underline { a }  }^{ T } $$

* Properties of the projection matrix P
    * The columnspace of P (C(P)) is the line which contains **a**
    * The rank is 1, rank(P) = 1
    * P is symmetrix, i.e. PT = P
    * Applying the projection matrix a second time (i.e. P2) nothing changes, thus P2 = P

## Why project?

(projecting onto more than a one-dimensional line)

* Because A**x** = **b** may not have a solution
    * **b** may not be in the columnspace
    * May have more equations than unknowns
* Solve for the closest vector in the columnspace
    * This is done by solving for **p** instead, where **p** is the projection of **b** onto the columnsapce of A
$$ A\hat { x } =\underline { p }  $$

* Now we have to get **b** orthogonally project (as **p**) onto the column(sub)space
* This is done by calculating two bases vectors for the plane that contains **p**, i.e. **a**1 and **a**2

* Going way back to the graph up top we note that **e** is perpendicular to the plane
* So, we have:
$$ A\hat { x } =\underline { p } $$
* We know that both **a**1 and **a**2 is perpendicular to **e**, so:
$$ { a }_{ 1 }^{ T }\underline { e } =0;\quad { a }_{ 2 }^{ T }\underline { e } =0\\ \because \quad \underline { e } =\underline { b } -\underline { p } \\ \because \quad \underline { p } =A\hat { x } \\ { a }_{ 1 }^{ T }\left( \underline { b } -A\hat { x }  \right) =0;\quad { a }_{ 2 }^{ T }\left( \underline { b } -A\hat { x }  \right) =0 $$

* We know that from ...
$$ \begin{bmatrix} { a }_{ 1 }^{ T } \\ { a }_{ 2 }^{ T } \end{bmatrix}\left( \underline { b } -A\hat { x }  \right) =\begin{bmatrix} 0 \\ 0 \end{bmatrix}\\ { A }^{ T }\left( \underline { b } -A\hat { x }  \right) =0 $$
* ... **e** must be in the nullspace of AT
* Which is right because from the previous lecture the nullspace of AT is orthogonal to the columnspace of A

* Simplifying the last equations we have
$$ {A}^{T}{A} \hat{x} = {A}^{T}{b} $$

* Just look back at the plane example in ℝ2 example we started with
* Simplifying things back to a column vector **a** instead of a matrix subspace A in this last equation does give us what we had in ℝ2

* Solving this we have
$$ \hat { x } ={ \left( { A }^{ T }A \right)  }^{ -1 }{ A }^{ T }\underline { b }  $$

* Which leaves us with
$$ \underline { p } =A\hat { x } \\ \underline { p } =A{ \left( { A }^{ T }A \right)  }^{ -1 }{ A }^{ T }\underline { b }  $$

* Making the projection matrix P
$$ P=A{ \left( { A }^{ T }A \right)  }^{ -1 }{ A }^{ T } $$

* Just note that for a square invertible matrix A, P is the identity matrix
* Most of the time A is not square (and thus invertible) so we have to leave the equation as it is
* Also, note that PT = P and P2 = P

## Applications

### Least squares

* Given a set of data points in two dimensions, i.e. with variables (*t*,*b*)
* We need to fit them onto the best line
* So, as an example consider the points (1,1), (2,2), (3,2)

* A best line in this instance means a straight line in the form
$$ {b}={C}+{D}{t} $$
* Using the three points above we get three equations
$$ {C}+{D}=1 \\ {C}+{2D} = 2 \\ {C}+{3D}=2 $$

* If the line goes through all points, we would give a solution
* Instead we have the following
$$ \begin{bmatrix} 1 & 1 \\ 1 & 2 \\ 1 & 3 \end{bmatrix}\begin{bmatrix} C \\ D \end{bmatrix}=\begin{bmatrix} 1 \\ 2 \\ 2 \end{bmatrix} $$
* Three equation, two unknowns, no solution, **so** solve ...
$$ { A }^{ T }A\hat { x } ={ A }^{ T }b $$
* ... which for the solution is
$$ \hat { x } ={ \left( { A }^{ T }A \right)  }^{ -1 }{ A }^{ T }b $$

```python
A = Matrix([[1, 1], [1, 2], [1, 3]])
A
```

$$\left[\begin{matrix}1 & 1\\1 & 2\\1 & 3\end{matrix}\right]$$

```python
b = Matrix([1, 2, 2])
b
```

$$\left[\begin{matrix}1\\2\\2\end{matrix}\right]$$

```python
(A.transpose() * A).inv() * A.transpose() * b
```

$$\left[\begin{matrix}\frac{2}{3}\\\frac{1}{2}\end{matrix}\right]$$

* Thus, the solution is:
$$ b=\frac { 2 }{ 3 } +\frac { 1 }{ 2 } t $$

```python

```

Mathematics/Linear Algebra/0.0 MIT-18.06 - Jupyter/Lecture_15_Projection_onto_subspaces.ipynb