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

Unidade Astronômica $= 1,5 \cdot 10^8 km$

Massa da Terra $= 5,972 \cdot 10^{24} kg$

Massa do Sol $ = 1,989 \cdot 10^{30} kg$

Constante Gravitacional = $6.67408 \cdot 10^{-11} m^{3} kg^{-1} s^{-2}$

$$
F = ma
$$

$$
F = G\frac{m_1m_2}{||r^2||}
$$

\begin{align}
m_1\ddot{r} &= G\frac{m_1m_2}{||r^2||}\\
\end{align}
Com $r = |x_1 - x_2|$, como queremos o vetor na direção do astro, vamos multiplicar pelo unitario de r

$$
m_1\ddot{r} = G\frac{m_1m_2}{||r^2||}\frac{r}{||r||}
$$

$$
m_1\ddot{r} = G\frac{r m_1m_2}{||r^3||}
$$

```python
G = 6.67408
```

```python
u = [1,0,0,0,0.1,0.2,0,0]
m = [1.989*10**6,5.972]
du = [u[2],u[3],G*m[1]*(u[4]-u[0])/np.sqrt((u[4]-u[0])**2 + (u[5]-u[1])**2), G*m[0]*(u[5]-u[1])/np.sqrt((u[4]-u[0])**2 + (u[5]-u[1])**2),u[6],u[7],G*m[0]*(u[4]-u[0])/np.sqrt((u[4]-u[0])**2 + (u[5]-u[1])**2),G*m[1]*(u[5]-u[1])/np.sqrt((u[4]-u[0])**2 + (u[5]-u[1])**2)]
```

```python
def F(u,t):
    return np.array([u[2],u[3],G*m[1]*(u[4]-u[0])/np.sqrt((u[4]-u[0])**2 + (u[5]-u[1])**2), G*m[0]*(u[5]-u[1])/np.sqrt((u[4]-u[0])**2 + (u[5]-u[1])**2),u[6],u[7],G*m[0]*(u[4]-u[0])/np.sqrt((u[4]-u[0])**2 + (u[5]-u[1])**2),G*m[1]*(u[5]-u[1])/np.sqrt((u[4]-u[0])**2 + (u[5]-u[1])**2)])
```

```python
def rk_2(F, y0, ts):
    ys = [y0]
    t = ts[0]
    h = ts[1] - ts[0]
    for tnext in ts:
        k1 = F(ys[-1], t)
        k2 = F(ys[-1] + h*k1,tnext)
        ynext = ys[-1] + h * (k1+k2) / 2.0
        ys.append(ynext)
        t = tnext
    return np.array(ys[:-1])
```

```python
ts = np.linspace(-25000,25000,3000)
ys = rk_2(F,u,ts)
len(ys)
```

    3000

```python
ys[2][1]
```

    800449248.8046808

```python
x1 = [i[0] for i in ys]
y1 = [i[1] for i in ys]
x2 = [i[4] for i in ys]
y2 = [i[5] for i in ys]
```

```python
plt.plot(x1,y1)
```

```python
m = [1.989*10**30,5.972*10**24]
G = 6.67408*10**(-11)
d = 1.5*10**(8)
```

```python
def model(r,t):
    vdot,v = r
    return [v,G * m[0]/r[0]**2]
```

```python
model([2,3],5)
```

    [3, 3.31868628e+19]

```python
plt.polar(1,np.pi/2,'ro')
plt.show()
```

```python
def G(p):
    return [1]*500
```

```python
ps = np.linspace(0,2*np.pi,500)
```

```python
plt.polar(ps,G(ps))
```

```python
def X(p):
    return 2*np.cos(p)
def Y(p):
    return 2*np.sin(p)
```

```python
plt.plot(Y(ps),X(ps))
plt.show()
```

```python
r0 = [1.496*10**11,0]
t0 = [np.pi/6,1.990989*10**(-7)]
Gconst = 6.67408*10**(-11)
```

```python
def modelR(r,t):
    r2,rdot = r
    rnew = r[0]*t[1]**2 - Gconst * m[0]*r[0]**2
    return [rdot, rnew]

def modelT(r,t):
    t,tdot = t
    tnew = -2*r[1]*t[1]/r[0]
    return [tdot,tnew]
```

```python
r3 = modelR(r0,t0)
```

```python
paramsR = [[1.496*10**11],[0]]
paramsT = [[np.pi/6],[1.990989*10**(-7)]]

m = [1.9*10**30,5.972*10**24]
Gconst = 6.67408*10**(-11)

def RDDOT():
    return paramsR[0][-1]*paramsT[1][-1]**2 - Gconst * m[0]/paramsR[0][-1]

def TDDOT():
    return -2.0*paramsR[1][-1]*paramsT[1][-1]/paramsR[0][-1]
```

```python
def newvalue(curr, dt, acc):
    return curr + dt*acc
```

```python
dt = 90
for i in range(20):
    dist = RDDOT()
    paramsR[1].append(newvalue(paramsR[1][-1],dt,dist))
    paramsR[0].append(newvalue(paramsR[0][-1],dt,dist))
    angle = TDDOT()
    paramsT[1].append(newvalue(paramsT[1][-1],dt,angle))
    paramsT[0].append(newvalue(paramsT[0][-1],dt,angle))
```

```python
plt.figure(figsize=(10,20))
plt.polar(paramsR[0],paramsT[0])
plt.show()
```

```python
def X(r,t):
    return r*np.cos(t)
def Y(r,t):
    return r*np.sin(t)
```

```python
plt.plot(Y(paramsR[1],paramsT[1]),X(paramsR[1],paramsT[1]))
```

```python
X(paramsR[0][-1],paramsT[0][-1])
```

    nan

```python
X(paramsR[0][0],paramsT[0][0])
```

    129557400406.15202

```python
paramsR[0][-1]-paramsR[0][0]
```

    nan

### Two body problem

$$
\ddot{x} = -\frac{1+q}{r^3}x
$$

$$
\ddot{y} = -\frac{1+q}{r^3}y
$$

$$
q = \frac{m}{M}
$$

\begin{cases}
u_1 = x\\
u_2 = y\\
u_3 = \dot{x}\\
u_4 = \dot{y}
\end{cases}

E

\begin{cases}
\dot{u_1} = u_3\\
\dot{u_2} = u_4\\
\dot{u_3} = -\frac{1+q}{r^3}x\\
\dot{u_4} = -\frac{1+q}{r^3}y
\end{cases}

```python
u = [1,0,2,3]
q = 0.5
e = 0.7

rx = [1,0]
ry = [0,0]

def model(y=0,t=0):
    du = [0,0,0,0]
    r = u[:2]
    norm_r = np.sqrt(r[0]**2 + r[1]**2)
    for i in range(2):
        du[i] = u[i+2]
        du[i+2] = (-1+q)*r[i]/norm_r**3
    return du

```

```python
model()
```

    [2, 3, -0.5, -0.0]

```python
def rk_2(F, y0, ts):
    ys = [y0]
    t = ts[0]
    h = ts[1] - ts[0]
    for tnext in ts:
        k1 = F(ys[-1], t)
        k2 = F(ys[-1] + h*k1,tnext)
        ynext = ys[-1] + h * (k1+k2) / 2.0
        ys.append(ynext)
        t = tnext
    return np.array(ys[:-1])
```

```python
def rk_4_step(F,y0,t0,h):
    k1 = h*F(y0,t0)
    k2 = h*F(y0+k1/2,t0+h/2)
    k3 = h*F(y0+k2/2,t0+h/2)
    k4 = h*F(y0+k3,t0+h)
    ynext = y0 + k1/6 + k2/3 + k3/3 + k4/6
    return ynext
```

```python

```


# The shallow water equations

In this chapter we study a model for shallow water waves in one dimension:
\begin{align}
    h_t + (hu)_x & = 0 \label{SW_mass} \\
    (hu)_t + \left(hu^2 + \frac{1}{2}gh^2\right)_x & = 0. \label{SW_mom}
\end{align}
Here $h$ is the depth, $u$ is the velocity, and $g$ is a constant representing the force of gravity.  These equations are "depth averaged" and neglect vertical velocity and any vertical variations in the horizontal velocity.  Viscosity and compressibility are neglected, and the pressure is assumed to be hydrostatic.  Nevertheless, this is a surprisingly effective model for many applications, particularly when the wavelength is long compared to the fluid depth.

Previously we have looked at a *linear hyperbolic system* ([acoustics](Acoustics.ipynb)) and *scalar hyperbolic equations* ([advection](Advection.ipynb), [Burgers' equation](Burgers.ipynb), [traffic flow](traffic.ipynb)).  The shallow water system is our first example of a *nonlinear hyperbolic system*; solutions of the Riemann problem for this system consist of two waves (since it is a system of two equations), each of which may be a shock or rarefaction (since it is nonlinear).

## Hyperbolic structure
We can write (\ref{SW_mass})-(\ref{SW_mom}) in the canonical form $q_t + f(q)_x = 0$ if we define
\begin{align}
q & = \begin{pmatrix} h \\ hu \end{pmatrix} & f & = \begin{pmatrix} hu \\ hu^2 + \frac{1}{2}gh^2 \end{pmatrix}.
\end{align}
In terms of the conserved quantities, the flux is
\begin{align}
f(q) & = \begin{pmatrix} q_2 \\ q_2^2/q_1 + \frac{1}{2}g q_1^2 \end{pmatrix}
\end{align}
Thus the flux jacobian is
\begin{align}
f'(q) & = \begin{pmatrix} 0 & 1 \\ -(q_2/q_1)^2 + g q_1 & 2 q_2/q_1 \end{pmatrix}
        = \begin{pmatrix} 0 & 1 \\ -u^2 + g h & 2 u \end{pmatrix}.
\end{align}
Its eigenvalues are
\begin{align} \label{SW:char-speeds}
    \lambda_1 & = u - \sqrt{gh} & \lambda_2 & = u + \sqrt{gh},
\end{align}
with corresponding eigenvectors
\begin{align} \label{SW:fjac-evecs}
    r_1 & = \begin{bmatrix} 1 \\ u-\sqrt{gh} \end{bmatrix} &
    r_2 & = \begin{bmatrix} 1 \\ u+\sqrt{gh} \end{bmatrix}
\end{align}
Notice that -- unlike for acoustics, but similar to the LWR traffic model -- the eigenvalues and eigenvectors depend on $q$.  Because of this, the waves appearing in the Riemann problem move at different speeds and may be either jump discontinuities (shocks) or smoothly-varying  rarefaction waves.

```python
%matplotlib inline
```

```python
%config InlineBackend.figure_format = 'svg'
from ipywidgets import interact
from ipywidgets import widgets, FloatSlider, Checkbox, RadioButtons, fixed
from exact_solvers import shallow_water
from exact_solvers import shallow_demos
```

If you wish to examine the Python code for this chapter, see:
 - [exact_solvers/shallow_water.py](exact_solvers/shallow_water.py)
 - [exact_solvers/shallow_demos.py](exact_solvers/shallow_demos.py)

We set the gravitational constant to $g=1$ for the nondimensional form of the equations, as used for example in (LeVeque, 2002):

```python
g = 1.
```

## The Riemann problem
Consider the Riemann problem with left and right states

\begin{align*}
q_l & = \begin{bmatrix} h_l \\ h_r u_l \end{bmatrix}, &
q_r & = \begin{bmatrix} h_r \\ h_r u_r \end{bmatrix}.
\end{align*}

Typically the Riemann solution consists of two waves, one related to each of the eigenvectors in (\ref{SW:fjac-evecs}).  We refer to the wave related to $r_1$ (the first characteristic family) as a 1-wave and the wave related to $r_2$ (the second characteristic family) as a 2-wave.  Each wave may be a shock or rarefaction.  There is a constant intermediate state $q_m$ between them.  In the figure below, we illustrate a typical situation in which the 1-wave happens to be a rarefaction and the 2-wave is a shock:

In the figure we have one wave going in each direction, but since the wave speeds depend on $q$ and can have either sign, it is possible to have both waves going left, or both going right.

To solve the Riemann problem, we must find $q_m$.  To do so we must find a state that can be connected to $q_l$ by a 1-shock or 1-rarefaction and to $q_r$ by a 2-shock or 2-rarefaction.  We must also ensure that the resulting waves satisfy the entropy condition.

## Shock waves
Let us now consider a shock wave connecting a known state $q_*=(h_*, h_* u_*)$ to an unknown state $q=(h,hu)$.  In the context of the Riemann problem, $q_*$ will be the known left or right state and $q$ will be the unknown middle state.
The Rankine-Hugoniot jump conditions $s(q_* - q) = f(q_*) - f(q)$ for a shock wave moving
at speed $s$ read

\begin{align}
s (h_* - h) & = h_* u_* - h u \\
s (h_* u_* - h u) & = h_* u_*^2 - h u^2 + \frac{g}{2}(h_*^2 - h^2).
\end{align}

It is convenient to define $\alpha = h - h_*$.  Then we can combine the two equations above to show that the momenta are related by

\begin{align} \label{SW:hugoniot-locus}
    h u & = h_* u_* + \alpha \left[u_* \pm \sqrt{gh_* \left(1+\frac{\alpha}{h_*}\right)\left(1+\frac{\alpha}{2h_*}\right)}\right]
\end{align}

for a shock moving with speed

\begin{align} \label{SW:shock-speed}
    s & = \frac{h_* u_* - h u}{h_* - h} = u_* \mp \sqrt{gh_* \left(1+\frac{\alpha}{h_*}\right)\left(1+\frac{\alpha}{2h_*}\right)} = u_* \mp \sqrt{gh \frac{h_* + h}{2h_*} }
\end{align}

Choosing the plus sign in (\ref{SW:hugoniot-locus}) (which yields the minus sign in (\ref{SW:shock-speed})) gives a 1-shock.  Choosing the minus sign in (\ref{SW:hugoniot-locus}) (which yields the plus sign in (\ref{SW:shock-speed})) gives a 2-shock.  Notice from the last expression in (\ref{SW:shock-speed}) that as $h \to h^*$ the shock speed approaches the corresponding characteristic speed (\ref{SW:char-speeds}), while the ratio of the jumps in momentum and depth approaches that suggested by the eigenvectors (\ref{SW:fjac-evecs}).

We can now consider the set of all possible states $(h, h u)$ satisfying (\ref{SW:hugoniot-locus}).  This curve in the $h - hu$ plane is known as the hugoniot locus; there is a family of hugoniot loci for 1-shocks and another for 2-shocks.  Below we plot some members of the 1-loci of curves.  *In the interactive notebook you can select the 2-loci instead.*

```python
interact(shallow_water.plot_hugoniot_loci, y_axis=widgets.fixed('hu'),
         plot_1=widgets.Checkbox(description='Plot 1-loci',value=True),
         plot_2=widgets.Checkbox(description='Plot 2-loci',value=False));
```

The plot above shows the Hugoniot loci in the $h$-$hu$ plane, the natural phase plane in terms of the two conserved quantities.  Note that they all approach the origin as $h \rightarrow 0$. Alternatively, we can plot these same curves in the $h$-$u$ plane:

```python
interact(shallow_water.plot_hugoniot_loci, y_axis=widgets.fixed('u'),
         plot_1=widgets.Checkbox(description='Plot 1-loci',value=True),
         plot_2=widgets.Checkbox(description='Plot 2-loci',value=False));
```

Note that in this plane, the curves in each family are simply vertical translates of one another, and all curves asymptote to $\pm \infty$ as $h \rightarrow 0$.

### All-shock Riemann solutions
If we knew *a priori* that both waves in the Riemann solution were shock waves, we could find $q_m$ by finding the intersection of the 1-Hugoniot locus passing through $q_l$ with the 2-Hugoniot locus passing through $q_r$.  The Riemann solution would then consist simply of two discontinuities propagating at the speeds given by (\ref{SW:shock-speed}).

Here is an example for which this is the correct solution: The initial depth is the same everywhere, $h_l=h_r$, but the velocity $u_l>0$ while $u_r<0$ so the flow is toward $x=0$ from both sides.  Note that in this plot the dark and light blue stripes show a passive tracer advected with the flow, which is shown only to help visualize the fluid velocity in these plots.

```python
shallow_water.plot_riemann_SW(h_l=2, h_r=2, u_l=1, u_r=-1)
```

*In the interactive notebook you can check the boxes to plot the characteristic families, and notice that the 1-characteristics impinge on the 1-shock; the 2-characteristics impinge on the 2-shock.  Thus these shocks satisfy the entropy condition.*

#### Dam-break problem: all-shock solution
Here's another example, which you might think of as modeling a dam that suddenly breaks.  The water is initially at rest on both sides, but much deeper on the left.  We can again construct a 2-shock Riemann solution, but *this is not physically correct in this case!*

```python
shallow_water.plot_riemann_SW(h_l=4, h_r=1, u_l=0, u_r=0, force_waves='shock');
```

*With the interactive widget, you can check the box to plot the 1-characteristics.*  Notice that they don't impinge on the 1-shock; instead, characteristics are diverging away from it.  This shock does not satisfy the entropy condition and should be replaced with a rarefaction.  The corresponding part of the Hugoniot locus is plotted with a dashed line to remind us that it is unphysical.

*Now check the box to plot the 2-characteristics.*  Notice that these *do* impinge on the 2-shock; this is an entropy-satisfying shock, and the Hugoniot locus is plotted as a solid line.

### The entropy condition
For the 2-shock Riemann solution to be physically correct, each of the waves must satisfy the Lax entropy condition, with the characteristic speed to the left of the wave greater than that to the right.  Notice that the shock speed (\ref{SW:shock-speed}) can be written as

\begin{align}
    s & = u_* \pm \sqrt{gh \frac{h_* + h}{2h_*} }
\end{align}
Thus the 1-shock entropy condition reads

\begin{align*}
u_l - \sqrt{gh_l} > u_l - \sqrt{gh_l \frac{h_l+h_m}{2h_l}} = u_m - \sqrt{gh_m \frac{h_m+h_l}{2h_m}} > u_m - \sqrt{gh_m}.
\end{align*}
The second expression for the shock speed is obtained by noticing
that the shock speed is invariant under transposition of the left and right states.
The first and second inequalities both simplify to

\begin{align} \label{SW:1-entropy}
    h_m > h_l.
\end{align}
Similarly, it can be shown that the entropy condition for a 2-shock connecting $q_m$ and $q_r$ is satisfied if and only if

\begin{align} \label{SW:2-entropy}
    h_m > h_r.
\end{align}
If (\ref{SW:1-entropy}) or (\ref{SW:2-entropy}) is violated, then the
corresponding wave must be a rarefaction rather than a shock.

Go back and look at the phase plane plots above.  You'll notice that the 1-Hugoniot locus passing through $q_l$ is plotted with a solid line for $h>h_l$ and with a dashed line for $h<h_l$ (the same is done for the 2-Hugoniot locus passing through $q_r$).  This is to remind us that states connected to $q_l$ by the dashed part are unphysical.

## Rarefaction waves

### Integral curves
In the LWR traffic flow model, we saw that a rarefaction wave
consisted of a smoothly-varying density.  For the shallow water
system, both the depth $h$ and the momentum $hu$ will vary smoothly
through a rarefaction wave.  A given rarefaction wave is associated
with just one characteristic family, and the variation within the
wave is at all points proportional to the corresponding eigenvector
$r_p$.  In other words, all values $\tilde{q}=(h,hu)$ in the rarefaction
lie on a curve defined by

\begin{align} \label{SW:gen_ode}
    \tilde{q}'(\xi) & = r_p(\tilde{q}(\xi)).
\end{align}

Here $\tilde{q}(\xi)$ is a parameterization of the solution.
For the shallow water system, (\ref{SW:gen_ode}) reads

\begin{align}
    h'(\xi)    = q_1'(\xi) & = 1 \label{SW:dh} \\
    (hu)'(\xi) = q_2'(\xi) & = u \pm \sqrt{gh} = \tilde{q}_2/\tilde{q}_1 - \sqrt{g\tilde{q}_1}, \label{SW:dhu}
\end{align}
where we take the minus sign for 1-waves and the plus sign for 2-waves.
We can think of (\ref{SW:dh})-(\ref{SW:dhu}) as an initial value ODE;
fixing the value of $\tilde{q}$ at one point in the rarefaction wave
determines the whole solution of (\ref{SW:dh})-(\ref{SW:dhu}).  We
refer to each of these solutions as an *integral curve*.

For the shallow water system, these equations can be integrated exactly.  If we fix one point $(h_*, h_*u_*)$ on the curve, the whole integral curve is defined by

\begin{align}
    hu & = hu_* \pm 2h(\sqrt{gh_*} - \sqrt{gh}).
\end{align}
where now the plus sign is for 1-waves and the minus sign for 2-waves.
This can be rewritten as

\begin{align} \label{SW:riemann-invariant}
u \pm 2 \sqrt{gh} = u_* \pm 2 \sqrt{gh_*}.
\end{align}
From (\ref{SW:riemann-invariant}) we see that the value $u + 2 \sqrt{gh}$
is constant along any 1-integral curve, while the value $u-2\sqrt{gh}$ is constant along any 2-integral curve.  We refer to these quantities as *Riemann invariants* for the shallow water system:

\begin{align}
w_1(q) & = u + 2 \sqrt{gh} \\
w_2(q) & = u - 2 \sqrt{gh}.
\end{align}
In other words, the trajectories plotted above are just the level curves of $w_1$ and $w_2$.  This allows us to easily plot the integral curves as a contour plot of $w_1$ or $w_2$.  *In the notebook we define a function to do this and the widget allows selecting which family of curves to display.*

```python
interact(shallow_demos.plot_int_curves, y_axis=widgets.fixed('hu'),
         plot_1=widgets.Checkbox(description='1-wave curves',
                                 value=True),
         plot_2=widgets.Checkbox(description='2-wave curves',
                                 value=False));
```

We can also plot the integral curves in the $h$--$u$ plane:

```python
interact(shallow_demos.plot_int_curves, y_axis=widgets.fixed('u'),
         plot_1=widgets.Checkbox(description='1-wave curves',
                                 value=True),
         plot_2=widgets.Checkbox(description='2-wave curves',
                                 value=False));
```

Note that in this plane the integral curves of each family are simply vertical translates of one another due to the form of the functions $w_1$ and $w_2$.  Unlike the Hugoniot loci, the integral curves do not asymptote to $\pm\infty$ as $h \rightarrow 0$ and instead approach a finite value.

### Comparison of integral curves and Hugoniot loci
You may have noticed that the integral curves look very similar to the Hugoniot loci, especially when plotted in the $h-hu$ plane.  Below we emphasize the differences by plotting the curve from each of these families that passes through a specified point.  The difference is less noticeable in the $h-hu$ plane since all of the curves approach the origin.

The `compare_curves` function plots these two curves together. *In the notebook you can select which family to display.*

```python
interact(shallow_demos.compare_curves,
         wave_family=RadioButtons(options=[1,2],
                                  description='Wave family:'),
         y_axis=RadioButtons(options=['u','hu'],
                             description='Vertical axis:'),
         h0=FloatSlider(min=1.e-1,max=3.,value=1.,
                        description='$h_*$'),
         u0=FloatSlider(min=-3,max=3,description='$u_*$'));
```

Near the point of intersection, the curves are very close; indeed, they must be tangent at this point since their direction is parallel to the corresponding eigenvector there (and in fact they also have the same curvature).  Far from this point they diverge; for small depths they must diverge greatly, since the Hugoniot locus never reaches $h=0$ at any finite velocity.

### The structure of centered rarefaction waves
Rarefactions appearing in the Riemann solution are also similarity solutions; that is they are constant along any ray $\xi=x/t$.  For a $p$-rarefaction connecting states $q_l$ and $q_r$, the rarefaction thus has the form

\begin{align}
    q(x,t) & = \begin{cases} q_l & \text{if } x/t \le \lambda_p(q_l) \\
                            \tilde{q}(x/t) & \text{if } \lambda_p(q_l) \le \lambda_p(q_r) \\
                            q_r & \text{if } x/t \ge \lambda_p(q_r). \end{cases}
\end{align}

It remains to be determined how $\tilde{q}$ varies as a function of $x/t$.  This can be determined by noting that each solution value $\tilde{q}$ propagates at the characteristic speed $\lambda_p(\tilde{q})$.  Letting $\xi=x/t$, this gives us the condition

\begin{align} \label{SW:char-sim}
    \lambda_p(\tilde{q}(\xi)) = \xi.
\end{align}

By combining (\ref{SW:char-sim}) with the Riemann invariant condition, we can find $\tilde{h}$ and $\tilde{u}$ within the rarefaction.  For concreteness, let us consider a 1-rarefaction connecting $q_l$ and $q_m$.  Then (\ref{SW:char-sim}) reads $\tilde{u} - \sqrt{g\tilde{h}} = \xi$, which simplifies to

\begin{align} \label{SW:char-sim1}
\tilde{h} = \frac{(\tilde{u}-\xi)^2}{g}.
\end{align}
Meanwhile, the fact that $w_1(\tilde{q})$ is constant means that

\begin{align} \label{SW:w1-const}
    \tilde{u} + 2 \sqrt{g\tilde{h}} = u_l + 2 \sqrt{gh_l}.
\end{align}
Combining (\ref{SW:char-sim1}) with (\ref{SW:w1-const}), we find

\begin{align} \label{SW:1-raref-struct-h}
    \tilde{h} & = \frac{(u_l + 2\sqrt{gh_l} - \xi)^2}{9g} = \frac{(w_1(q_l) - \xi)^2}{9g} \\
    \tilde{u} & = \frac{1}{3}w_1(q_l) + \frac{2}{3}\xi. \label{SW:1-raref-struct-u}
\end{align}
We see that $h$ varies quadratically through the rarefaction, while $u$ varies linearly.

### Connecting states
Suppose we know that the middle state $q_m$ is connected to the left and right states by rarefactions.  Then we can find $q_m$ by finding the intersection of the corresponding integral curves.  Here is an example for which this is the physically correct solution:

\begin{align}
    q_l & = \begin{bmatrix} 1 \\ -1 \end{bmatrix} &
    q_r & = \begin{bmatrix} 1 \\ 1 \end{bmatrix}
\end{align}

In this case the water is flowing *away* from $x=0$ on both sides.

```python
shallow_water.plot_riemann_SW(h_l=1, h_r=1, u_l=-1., u_r=1.)
```

Notice that the segment of each integral curve that connects to states with a smaller depth is plotted as a solid line, while the segment connecting to states with greater depth is plotted with a dashed line.  This again is to remind us that states connected by a rarefaction through the solid part are physical (entropy-satisfying), while states connected by the dashed part would be unphysical (entropy-violating).

*Try changing the velocities to $(1.9, -1.9)$, and notice that the Riemann solution has a state with almost zero depth in the middle.  What happens if you use even larger outflow velocities?  We'll discuss this later, in the section on dry states.*

### Dam-break problem: all-rarefaction solution
Next let us revisit the problem from [the section above](Shallow_water.ipynb#Dam-break-problem:-all-shock-solution) where we found that the all-shock solution was incorrect.  This time we'll try to find an all-rarefaction solution.

```python
shallow_water.plot_riemann_SW(h_l=4, h_r=1, u_l=0, u_r=0, force_waves='raref')
```

Plot the 2-characteristics and notice that they impinge on the 2-rarefaction; if plotted within the rarefaction, they would cross. This means that the solution we constructed is triple-valued and nonsensical as a solution to this one-dimensional conservation law, and so this portion of the solution is omitted in the plots of depth and momentum.  In this case a rarefaction wave is not physical and should be replaced with a shock; the corresponding part of the integral curve is hence shown as a dashed line.

## The Riemann solution
To determine $q_m$, we need to know whether each of the two waves is a shock or rarefaction, so that we can use the appopriate Hugoniot locus or integral curve.  But to determine the nature of each wave, we need to check (\ref{SW:1-entropy}) or (\ref{SW:2-entropy}), which requires knowledge of $h_m$.  To deal with this we define a piecewise function $\phi_l(h)$ that gives:

- for $h>h_l$, the momentum connected to $q_l$ by a 1-shock;
- for $h<h_l$, the momentum connected to $q_l$ by a 1-integral curve.

Thus $\phi$ is partly a 1-Hugoniot locus and partly a 1-integral curve; it gives the value of each precisely in the regime where they are physically correct.  We can similarly define a function $\phi_r(h)$ related to the 2-Hugoniot locus and 2-integral curve.  The middle state depth $h_m$ is the value for which $\phi_l(h) = \phi_r(h)$.  This value can be found computationally via a rootfinding algorithm.

### Dam-break problem: correct solution
Below we plot (at last) the correct solution to the dam-break problem, involving a 1-rarefaction and a 2-shock.  Plot the characteristics and notice how each family behaves correctly with respect to the corresponding wave in the Riemann solution.

```python
shallow_water.plot_riemann_SW(h_l=4, h_r=1, u_l=0, u_r=0)
```

[This webpage](phase_plane_shallow_water_small.html) provides an interactive view that allows you to see how the solution changes as you move the left and right states around in the phase plane.  Note how the waves switch between shocks and rarefactions depending on the relative position of the left and right states.

## Dry states
The approach just described will yield the correct Riemann solution (provided that the rootfinding algorithm converges) in most situations.  However, it will fail if the solution involves dry ($h=0$) states.

### Dry middle state
Sometimes it seems that there is no way to connect a given pair of left and right states via entropy-satisfying shocks or rarefactions.  In the cell below we plot only the physically relevant parts of the integral curves and hugoniot loci passing through the left and right states.  Both the $h-u$ and $h-hu$ planes are shown.  *With the interactive widget you can try to adjust those states to find a combination for which none of the curves intersect.*

```python
interact(shallow_demos.connect_states,
         h_l=widgets.FloatSlider(min=0.001,max=2,value=1),
         u_l=widgets.FloatSlider(min=-5,max=5,value=-1),
         h_r=widgets.FloatSlider(min=0.001,max=2,value=1),
         u_r=widgets.FloatSlider(min=-5,max=5,value=1));
```

You should find that by making the initial states flow sufficiently fast away from each other, there is no intersection in the $h-u$ plane.  In the $h-hu$ plane, the integral curves always intersect at the origin.  The reason for this ambiguity is that for zero depth it isn't meaningful to assign a velocity.  Thus in the $h-u$ plane we could think of the entire $u$-axis as being part of every integral curve.  That means that we can always connect the left and right states via an intermediate state with depth $h=0$ (a dry state).

Since the 1-integral curve through $q_l$ reaches $h=0$ at $u = u_l + 2 \sqrt{gh_l}$ and the 2-integral curve reaches $h=0$ at $u=u_r-2\sqrt{gh_r}$, we see that a dry middle state occurs if and only if $u_l + 2 \sqrt{gh_l} < u_r-2\sqrt{gh_r}$.

It is clear in this case that the solution must consist of two centered rarefaction waves, connecting the left and right states to the middle dry state.  These centered rarefactions still have the structure derived above; to complete the solution we only need to know the range of centered characteristics included in each rarefaction.  This is most conveniently determined by considering the $h-u$ plane.  The 1-integral curve passing through $q_l$ is given by $u = u_l + 2(\sqrt{gh_l}-\sqrt{gh_r})$; it reaches a dry state $h=0$ at $u=u_l + 2\sqrt{gh_l}$.  Similarly, the 2-integral curve passing through $q_r$ reaches a dry state $h=0$ at $u=u_r - 2\sqrt{gh_r}$.  Thus the dry state solution is given by

\begin{align}
    q(x,t) & = \begin{cases}
                        q_l &         \text{if } x/t \le u_l - \sqrt{gh_l} \\
                        \tilde{q}_\ell & \text{if } u_l -  \sqrt{gh_\ell} \le x/t \le u_l+2\sqrt{gh_l} \\
                        0 &           \text{if } u_l + 2\sqrt{gh_l} \le x/t \le u_r - 2\sqrt{gh_r} \\
                        \tilde{q}_r & \text{if } u_r - 2\sqrt{gh_r} \le x/t \le u_r+\sqrt{gh_r} \\
                        q_r &         \text{if } x/t \ge u_r + \sqrt{gh_r},
                       \end{cases}
\end{align}
where $\tilde{q}_\ell$ is given by (\ref{SW:1-raref-struct-h})-(\ref{SW:1-raref-struct-u}) and $\tilde{q}_r$ is given by a similar expression.

Here's an example of a Riemann problem whose solution involves a dry middle state:

```python
shallow_water.plot_riemann_SW(h_l=0.5, h_r=0.5, u_l=-1.9, u_r=1.9)
```

### Dry initial states
What if the left or right state in the Riemann problem is dry?  In this case, the solution consists only of a rarefaction wave connecting the non-dry state to $h=0$.

```python
shallow_water.plot_riemann_SW(h_l=1, h_r=0, u_l=0, u_r=0)
```

## Interactive phase plane
[This webpage](http://www.clawpack.org/riemann_book/html/phase_plane_shallow_water_small.html) provides an interactive view that allows you to see how the solution changes as you move the left and right states around in the phase plane.  While you're there, be sure to click and drag the dark circles indicating the values of $q_l$ and $q_r$ in the top left plot.  The time can also be adjusted by dragging the circle in the top right plot.

The hugoniot loci are plotted in red and the integral curves in blue.  The portion of each curve that satisfies the entropy condition is plotted as a solid line, while the entropy-violating parts are plotted as dashed lines.  The curves are plotted for both the left and right states.  Thus the function $\phi_l$ just described consists of the solid curve (blue and red parts) passing through the left state $q_l$; the function $\phi_r$ is the solid curve (blue and red parts) passing through $q_r$.  The intersection of these curves is labeled $q_m$.

## Interactive solution
Finally, the notebook contains code that generates an interactive plot showing the solution to the Riemann problem for arbitrary left and right states, including solutions with dry states.

```python
def plot_exact_riemann_solution(h_l=3.,u_l=0.,h_r=1.,u_r=0.,t=0.2, fig=0):
    plot_function = shallow_water.make_demo_plot_function(h_l,h_r,u_l,u_r,
                                                          hlim=(0,6),ulim=(-3,3))
    plot_function(t,fig)

interact(plot_exact_riemann_solution,
         h_l=FloatSlider(min=0.,max=5.,step=0.1,value=3.,
                                 description=r'$h_l$'),
         u_l=FloatSlider(min=-2.5,max=2.5,step=0.1,value=0.,
                                 description=r'$u_l$'),
         h_r=FloatSlider(min=0.,max=5.,step=0.1,value=1.,
                                 description=r'$h_r$'),
         u_r=FloatSlider(min=-2.5,max=2.5,step=0.1,value=0.,
                                 description=r'$u_r$'),
         t=FloatSlider(min=0., max=0.6, step=0.1,value=0.),
         fig=fixed(0));
```


# Constant pressure *ab initio* molecular dynamics with discrete variable representation basis sets

Zhonghua Ma and Mark Tuckerman, *J. Chem. Phys* **133** 184110 (2010)

Three dimensional FBR basis, defined in terms of general cell matrix $\mathbf{h}$.

Consider an invertible transformation between:
- a set of curvilinear coordinates $s_{\alpha}$, $\alpha = 1,2,3$, which has three axes along three box vectors of a general cell matrix, and
- a set of Cartesian coordinates $r_{\alpha}$, $\alpha = 1,2,3$

$$
\begin{equation}
r_{\alpha} = \sum_{j=1}^{3} h_{\alpha\beta}s_{\beta}
\end{equation}
$$

The columns of $\mathbf{h}$ are three box vectors $\mathbf{a}$, $\mathbf{b}$ and $\mathbf{c}$.

The above coordinate transformation maps a simulation cell of shape $\mathbf{h}$ into a unit cubic box.

Let's demonstrate this equation for several lattice constant.

We will generate points in scaled coordinate $s_{\beta}$ and get the actual grid points by multiplying them with cell matrix $h_{\alpha\beta}$

```julia
function gen_lattice_hexagonal(a; coa=8.0/3.0)
  LL = zeros(3,3)
  LL[1,:] = [1.0, 0.0, 0.0]
  LL[2,:] = [-cos(pi/3.0), sin(pi/3.0), 0.0]
  LL[3,:] = [0.0, 0.0, coa]
  return a*LL
end

function gen_lattice_fcc(a)
  LL = zeros(3,3)
  LL[1,:] = [ 0.0, 1.0, 1.0]
  LL[2,:] = [ 1.0, 0.0, 1.0]
  LL[3,:] = [ 1.0, 1.0, 0.0]
  return 0.5*a*LL
end
```

    gen_lattice_fcc (generic function with 1 method)

```julia
LL = gen_lattice_fcc(1.0)
a = LL[1,:]
b = LL[2,:]
c = LL[3,:]
LL
```

    3×3 Array{Float64,2}:
     0.0  0.5  0.5
     0.5  0.0  0.5
     0.5  0.5  0.0

Define general cell matrix $\mathbf{h}$

```julia
hh = zeros(3,3)
hh[:,1] = a
hh[:,2] = b
hh[:,3] = c
hh
```

    3×3 Array{Float64,2}:
     0.0  0.5  0.5
     0.5  0.0  0.5
     0.5  0.5  0.0

An old function function that I used to generate directly real space grid points

```julia
function init_r_grids( Ns, LatVecs )
  #
  Npoints = prod(Ns)
  #
  r = Array{Float64}(3,Npoints)
  ip = 0
  for k in 0:Ns[3]-1
    for j in 0:Ns[2]-1
      for i in 0:Ns[1]-1
        ip = ip + 1
        r[1,ip] = LatVecs[1,1]*i/Ns[1] + LatVecs[2,1]*j/Ns[2] + LatVecs[3,1]*k/Ns[3]
        r[2,ip] = LatVecs[1,2]*i/Ns[1] + LatVecs[2,2]*j/Ns[2] + LatVecs[3,2]*k/Ns[3]
        r[3,ip] = LatVecs[1,3]*i/Ns[1] + LatVecs[2,3]*j/Ns[2] + LatVecs[3,3]*k/Ns[3]
      end
    end
  end
  return r
end
```

    init_r_grids (generic function with 1 method)

```julia
function write_xsf( filnam, LL, atpos; atsymb=nothing, molecule=false )
  #
  f = open(filnam, "w")
  Natoms = size(atpos)[2]
  #
  if molecule
    @printf(f, "MOLECULE\n")
  else
    @printf(f, "CRYSTAL\n")
  end
  @printf(f, "PRIMVEC\n")
  @printf(f, "%18.10f %18.10f %18.10f\n", LL[1,1], LL[1,2], LL[1,3])
  @printf(f, "%18.10f %18.10f %18.10f\n", LL[2,1], LL[2,2], LL[2,3])
  @printf(f, "%18.10f %18.10f %18.10f\n", LL[3,1], LL[3,2], LL[3,3])
  @printf(f, "PRIMCOORD\n")
  @printf(f, "%8d %8d\n", Natoms, 1)
  #
  if atsymb == nothing
    for ia = 1:Natoms
      @printf(f, "X  %18.10f %18.10f %18.10f\n", atpos[1,ia], atpos[2,ia], atpos[3,ia])
    end
  else
    for ia = 1:Natoms
      @printf(f, "%s  %18.10f %18.10f %18.10f\n", atsymb[ia], atpos[1,ia], atpos[2,ia], atpos[3,ia])
    end
  end

  close(f)
end
```

    write_xsf (generic function with 1 method)

Let's do this first using my old function `init_r_grids`

```julia
Ns = [11,11,11]
r = init_r_grids(Ns,10*LL)
write_xsf("r_fcc.xsf", 10*LL, r)
```

Using the new way, first we need to generate grid of $s$:

```julia
function init_s_grid(Ns)
    if any( (Ns .% 2) .== 0 )
        error("Ns must be odd numbers")
    end
    NN = round.(Int, (Ns-1)/2)
    Npoints = prod(Ns)
    s = zeros(3,Npoints)
    ip = 0
    for n = -NN[3]:NN[3]
        for m = -NN[2]:NN[2]
            for l = -NN[1]:NN[1]
                ip = ip + 1
                s[1,ip] = l/(2*NN[1] + 1)
                s[2,ip] = m/(2*NN[2] + 1)
                s[3,ip] = n/(2*NN[3] + 1)
            end
        end
    end
    return s
end
```

    init_s_grid (generic function with 1 method)

Finally, a function to do the transformation from $s$ to $r$

```julia
function s_to_r(s::Array{Float64,2}, hh)
    Npoints = size(s)[2]
    r = zeros(3,Npoints)
    for ip = 1:Npoints
        for α = 1:3
            r[α,ip] = 0.0
            for β = 1:3
                r[α,ip] = r[α,ip] + hh[α,β]*s[β,ip]
            end
        end
    end
    return r
end
```

    s_to_r (generic function with 1 method)

Test for fcc and hexagonal grid:

```julia
println(10*hh)
r = s_to_r(s, 10*hh)
write_xsf("r_fcc_v2.xsf", 10*LL, r)
```

```julia
LL = gen_lattice_hexagonal(10)
hh = LL'
# Remember that Ns must be odd numbers
Ns = [11,11,15]
s = init_s_grid(Ns)
r = s_to_r(s,hh)
# don't forget to transpose hh because our convention is different from the one
# used in Ma and Tuckerman
write_xsf("r_hcp_v1.xsf", hh', r)
```

Periodic boundary conditions are used, plane-wave like functions
$\mathbf{\phi}_{\hat{\mathbf{k}}}(\mathbf{s})$ can be defined as functions of $s_{\alpha} \in [ -\dfrac{1}{2},\dfrac{1}{2} ]$ as

$$
\begin{equation}
\phi_{\hat{\mathbf{k}}}(\mathbf{s}) =
\left| \frac{\partial\mathbf{s}}{\partial\mathbf{r}} \right|^{1/2}
\exp\left[
2\pi\imath\hat{\mathbf{k}}\cdot\mathbf{s}
\right]
=
\dfrac{1}{\sqrt{\mathrm{det}(\mathbf{h})}}
\exp\left[
2\pi\imath\hat{\mathbf{k}}\cdot\mathbf{s}
\right]
\end{equation}
$$

where $\hat{\mathbf{k}} = (\hat{k}_1, \hat{k}_2, \hat{k}_3)$ is a vector of integers.

The basis $\{ \phi_{\hat{\mathbf{k}}}(\mathbf{s}) \}$ are orthonormal:

$$
\begin{equation}
\int
\,
\mathrm{d}\mathbf{s}
\,
\left|\frac{\partial\mathbf{r}}{\partial\mathbf{s}}\right|
\end{equation}
\phi^{*}_{\hat{\mathbf{k}}}(\mathbf{s})
\phi_{\hat{\mathbf{k}}'}(\mathbf{s})
=
\int
\,
\mathrm{d}\mathbf{s}
\,
\exp[ -2\pi\imath\hat{\mathbf{k}}\cdot\mathbf{s} ]
\,
\exp[  2\pi\imath\hat{\mathbf{k}}'\cdot\mathbf{s} ]
=
\delta_{\hat{\mathbf{k}},\hat{\mathbf{k}}'}
$$

We can define a FBR from the curvilinear plane-wave functions $\{\phi_{\hat{\mathbf{k}}}\}$, restricting:

$$
\begin{equation}
\hat{\mathbf{k}}_{\alpha} \in
\left[-\mathcal{N},\mathcal{N}\right]
\end{equation}
$$

where the size of DVR quadrature grid is $2\mathcal{N}+1$

The corresponding DVR functions $\mu_{lmn}(\mathbf{s})$ can be defined as

$$
\begin{equation}
\mu_{lmn}(\mathbf{s}) =
\sum_{\hat{\mathbf{k}}}
\phi_{\hat{\mathbf{k}}}(\mathbf{s})
\left\langle
\left.\phi_{\hat{\mathbf{k}}}\right|
\mu_{lmn}
\right\rangle
\end{equation}
$$

For periodic system, the scalar product $\left\langle
\left.\phi_{\hat{\mathbf{k}}}\right|\mu_{lmn}\right\rangle$ can be evaluated by $\mathcal{N}$-point Chebyshev-Gaussian quadrature of the first kind. An equally spaced grid in the curvilinear coordinates ensures the accuracy of the quadrature.

The DVR points $\mathbf{s}_{lmn}$ are

$$
\begin{equation}
\mathbf{s}_{lmn} = \left(
\frac{l}{2\mathcal{N}+1},
\frac{m}{2\mathcal{N}+1},
\frac{n}{2\mathcal{N}+1}
\right)
\end{equation}
$$

The DVR grid points in Cartesian coordinates are given by the matrix-vector product:

$$
\begin{equation}
\mathbf{r}_{lmn} = \mathbf{h}\mathbf{s}_{lmn}
\end{equation}
$$

The corresponding DVR functions $\mu_{lmn}(\mathbf{s})$ then become

$$
\begin{equation}
\mu_{lmn}(\mathbf{s}) =
\sum_{\hat{\mathbf{k}}}
\frac{1}{\sqrt{\left(2\mathcal{N}+1\right)^3}}
\left|
\frac{\partial\mathbf{s}}{\partial\mathbf{r}}
\right|^{1/2}
\cos\left[
2\pi\hat{\mathbf{k}}\cdot
\left(
\mathbf{s} - \mathbf{s}_{lmn}
\right)
\right]
\end{equation}
$$

$$
\begin{equation}
\left| \frac{\partial\mathbf{s}}{\partial\mathbf{r}} \right|^{1/2}
= \dfrac{1}{\sqrt{\mathrm{det}(\mathbf{h})}}
\end{equation}
$$

```julia
# Function to evaluate LF
function eval_μ_lmn(Ns, hh, s_grid, ip, s)
    #
    NN = round.(Int, (Ns-1)/2)
    metric = 1/sqrt(det(hh))
    #
    s_lmn = s_grid[:,ip]
    #
    f = 0.0 + im*0.0
    # loop structure should be the same as the one used in init_s_grid
    for kz = -NN[3]:NN[3]
        for ky = -NN[2]:NN[2]
            for kx = -NN[1]:NN[1]
                ks = dot( [kx,ky,kz], (s - s_lmn) )
                f = f + cos(2π*ks)
            end
        end
    end
    return metric/sqrt(prod(Ns))*f
end
```

    eval_μ_lmn (generic function with 2 methods)

```julia
# Sampling points
Ns = [21,21,11]

#LL = gen_lattice_hexagonal(10)
LL = 10.0*eye(3,3)  # cubic
hh = LL'

s_grid = init_s_grid(Ns)

ip = 2

#r = [5.0, 5.0, 5.0]
#s = inv(hh)*r
s = s_grid[:,1]
println( eval_μ_lmn(Ns, hh, s_grid, ip, s) )

s = s_grid[:,2]
println( eval_μ_lmn(Ns, hh, s_grid, ip, s) )
```

    8.003188339268475e-16 + 0.0im
    2.202498581157318 + 0.0im

With the above definition of the DVR functions, the overlap integrals of the DVR functions are equal to the Kronecker delta, and the values of $\mu_{lmn}(\mathbf{s})$ are zero at all DVR points except at $\mathbf{s}_{lmn}$

$$
\begin{equation}
\int\,\mathrm{d}\mathbf{s}\,
\left|
\frac{\partial\mathbf{r}}{\partial\mathbf{s}}
\right|
\mu_{lmn}(\mathbf{s})
\mu_{l'm'n'}(\mathbf{s})
= \delta_{ll'}\delta_{mm'}\delta_{nn'}
\end{equation}
$$

$$
\begin{equation}
\mu_{lmn}\left(\mathbf{s}_{l'm'n'}\right)
=
\delta_{ll'}\delta_{mm'}\delta_{nn'}
\sqrt{\frac{\left(2\mathcal{N}+1\right)^3}
{\left|\partial\mathbf{r}/\partial\mathbf{s}\right|}}
\end{equation}
$$

## Kinetic energy term

Laplacian operator:
$$
\begin{equation}
\nabla^2 = \sum_{\alpha=1}^{3}\sum_{\beta=1}^{3}\sum_{\gamma=1}^{3}
\frac{\partial}{\partial s_{\beta}}
\mathbf{h}^{-1}_{\beta\alpha}
\frac{\partial}{\partial s_{\gamma}}
\mathbf{h}^{-1}_{\gamma\alpha}
\end{equation}
$$

The kinetic energy can be computed as
$$
\begin{align}
E_{\mathrm{kin}} & = -\frac{1}{2}\sum_{i=1}^{N_{s}}
f_{i}
\left.
\left\langle
\psi_{i}
\right|
\nabla^2
\left|
\psi_{i}
\right\rangle
\right.
\\
& = -\frac{1}{2}\sum_{i=1}^{N_{s}}f_{i}
\sum_{l,l'=-\mathcal{N}}^{\mathcal{N}}
\sum_{m,m'=-\mathcal{N}}^{\mathcal{N}}
\sum_{n,n'=-\mathcal{N}}^{\mathcal{N}}
C_{lmn}^{i*} C_{l'm'n'}^{i}
\mathbb{L}_{lmn}^{l'm'n'}
\end{align}
$$

The matrix $\mathbb{L}_{lmn}^{l'm'n'}$ is given by
$$
\begin{align}
\mathbb{L}_{lmn}^{l'm'n'} & =
\sum_{\alpha=1}^{3}
\mathbf{h}_{1\alpha}^{-1}\mathbf{h}_{1\alpha}^{-1}
D_{ll'}\delta_{mm'}\delta_{nn'}
\\
& + \sum_{\alpha=1}^{3}
\mathbf{h}_{2\alpha}^{-1}\mathbf{h}_{2\alpha}^{-1}
D_{mm'}\delta_{ll'}\delta_{nn'}
\\
& + \sum_{\alpha=1}^{3}
\mathbf{h}_{3\alpha}^{-1}\mathbf{h}_{3\alpha}^{-1}
D_{nn'}\delta_{ll'}\delta_{mm'}
\end{align}
$$

where
$$
\begin{equation}
D_{jj'} = -4\pi^2
\left[
\frac{\mathcal{N}(\mathcal{N}-1)}{3}
\delta_{jj'}
+
\frac{(-1)^{j-j'}
\cos\left[
\dfrac{\pi(j-j')}{2\mathcal{N}+1}
\right]
}
{2\sin^2\left[
\dfrac{\pi(j-j')}{2\mathcal{N}+1}
\right]}
(1-\delta_{jj'})
\right]
\end{equation}
$$

```julia

```


Table of Contents

1  Introduction

1.1  Simulation

1.1.1  The Mesh

1.1.2  The Elements and DofHandlers

1.1.3  The Gaussian Field

1.1.4  Assembly

1.1.5  Solver

1.1.6  Quantity of Interest

2  Multiresolution Approximation

# Optimal Upscaling

```python
# Add src folder to path
import os
import sys

sys.path.insert(0,'../../src/')
```

## Introduction

Suppose $u$ satisfies the elliptic partial differential equation

\begin{align*}
-\nabla \cdot (\exp(q) \nabla u) = 1, \ \ &x \in D,\\
u = 0, \ \ &x \in \partial D
\end{align*}

where $D=[0,1]^2$ is a physical domain and $q(x,\omega)$ is a Gaussian random field. Moreover, let

\begin{equation}
J = \iint_{R}  u(x,\omega) dx, \ \ \text{where } R = [0.75,1]^2,
\end{equation}

be a physical quantity of interest for which we want to compute statistics using Monte Carlo sampling or whatever.

### Simulation

First, we show how to generate a single sample of $J$. Specifically, we will

1. Construct the computational mesh and define the elements
2. Generate samples of the random parameter $q$
3. Assemble the finite element system
4. Solve the system for $u$
5. Compute the associated $J$

We will use the ```quadmesh``` package, obtainable [here](https://github.com/hvanwyk/quadmesh).

#### The Mesh

The ```mesh``` module contains methods for generating and working with meshes. In this work, we use quadrilateral meshes, since they can be readily refined. On our mesh, we need to mark the boundary $\partial D$ and the region of integration $R$. To do this, we use marking functions that check whether a point lies in the given region.

```python
from mesh import QuadMesh
from plot import Plot

plot = Plot()

# Define the Quadrilateral Mesh
mesh = QuadMesh(resolution=(16,16))

# Mark boundary
bnd_fn = lambda x,y: abs(x)<1e-6 or abs(1-x)<1e-6 or abs(y)<1e-6 or abs(1-y)<1e-6
mesh.mark_region('bnd', bnd_fn, entity_type='half_edge', on_boundary=True)

# Mark averaging region
dmn_fn = lambda x,y: x>=0.75 and x<=1 and y>=0.75 and y<=1
mesh.mark_region('dmn', dmn_fn, entity_type='cell', strict_containment=True, on_boundary=False)
#cells = mesh.get_region(flag='dmn', entity_type='cell', on_boundary=False, subforest_flag=None)
plot.mesh(mesh, regions=[('bnd','edge'),('dmn','cell')])
```

#### The Elements and DofHandlers

Next we need to define the approximation space, i.e. the elements. This is done using the ```QuadFE``` class.
- Here we use piecewise constant elements (```DQ0```) to represent the parameter and piecewise linear elements (```Q1```) for the solution.
- Since we would like to have multiple different elements associated with the same mesh, we handle the element degrees of freedom by means of a **degrees-of-freedom-handler** or ```DofHandler```, which contains information of the ```mesh``` and the ```element```.
- For the assembly and function definitions, we may also require ```Basis``` functions. These also encode derivative information.

```python
from fem import QuadFE, DofHandler, Basis

#
# Elements
#
Q0 = QuadFE(mesh.dim(), 'DQ0')  # "Discontinuous Quadratic of Degree 0", for parameter
Q1 = QuadFE(mesh.dim(), 'Q1')  # "Continuous Linear" for output

#
# DofHandlers
#
dQ0 = DofHandler(mesh,Q0)
dQ1 = DofHandler(mesh,Q1)

#
# Distribute DOFs, i.e. store the dof-indices in the dofhandler
#
dQ0.distribute_dofs()
dQ1.distribute_dofs()

#
# Basis functions for assembly and definition of Nodal functions
#
phi_0 = Basis(dQ0)  # piecewise constant

phi_1 = Basis(dQ1)  # piecewise linear
phix_1 = Basis(dQ1,'ux')  # piecewise linear first partial w.r.t. x
phiy_1 = Basis(dQ1,'uy')  # piecewise linear first partial w.r.t. y
```

#### The Gaussian Field

The ```gmrf``` module contains routines to generate a variety of Gaussian random fields.
1. First, the covariance matrix is assembled from a bivariate kernel function using the ```Covariance``` class. The assembly is dependent on the mesh and approximation space (element) associated with the field.
2. Next we compute its eigendecomposition.
3. Finally we define a Gaussian random field using the ```GaussianField``` class, which allows us to sample, condition, etc.
4. Samples from the Gaussian field are given as vectors but we can use these to define ```Nodal``` interpolants on the given mesh.

```python
from function import Nodal
from gmrf import Covariance
from gmrf import GaussianField
import numpy as np

#
# Approximate Covariance Matrix
#
cov = Covariance(dQ0, name='gaussian', parameters={'l':0.01})
cov.compute_eig_decomp()

#
# Define the Gaussian field
#
q = GaussianField(dQ0.n_dofs(), K=cov)

# Sample Random field and store all samples in a Nodal DQ0 finite element function
n_samples = 100
eq = Nodal(basis=phi_0, data=np.exp(q.sample(n_samples)))

#
# Plot a single sample
#
plot.contour(eq, n_sample=20)
```

#### Assembly

The weak form of the elliptic PDE is given by

\begin{equation}\label{eq:weak_form}
\iint_D \exp(q) \nabla u \cdot \nabla \phi dx = \iint_D 1 \phi dx,
\end{equation}

for any test function $\phi$. Approximating $u = \sum_j c_j \phi_j$ in the Galerkin framework, we get $A c = b $, where

\begin{equation}\label{eq:bilinear}
A_{ij} = \iint_D \exp(q) \nabla \phi_j \cdot \nabla \phi_i dx = \iint_D \exp(q) \frac{\partial \phi_j}{\partial x} \frac{\partial \phi_i}{\partial x} + \exp(q)  \frac{\partial \phi_j}{\partial u}\frac{\partial \phi_i}{\partial y} dx
\end{equation}

and

\begin{equation}\label{eq:linear}
b_i = \iint_D \phi_i dx.
\end{equation}

The assembly therefore requires a bilinear form $A$ and a linear form $b$. We define each form with the ```Form``` class in the ```assembler``` module.

Each form requires a ```Kernel``` function, possibly a ```test``` function, and possibly a ```trial``` function (both of which should be ```Basis``` functions).

> For example, the linear form \ref{eq:linear} is defined by ```Form(1, test=phi_1)```, while the bilinear form $\iint_D \exp(q) \frac{\partial \phi_j}{\partial x} \frac{\partial \phi_i}{\partial x}dx$ is stored as ```Form(eq, test=phix_1, trial=phix_1)```.

1. The object ```Form(kernel)``` is assembled into a scalar.
2. The linear form ```Form(kernel, test=phi_1)``` is assembled into a vector.
3. The bilinear form ```Form(kernel, test=phix_1, trial=phix_1)``` is assembled into a sparse matrix.

Some forms, such as $\iint_R \phi_i dx$ should only be assembled over a subdomain of the mesh. To do this, use ```Form(1, test=phi_1, flag='dmn')```, where ```dmn``` was the flag marking the region $R$ from earlier.

> __REMARK:__ When the kernel is a function with multiple samples (such as our function ```eq```), then the relevant form is assembled for all samples.

The ```Assembler``` class handles finite element assembly. For each problem you want to assemble, e.g. the elliptic problem, or the problem of computing $J$, you list all relevant bilinear, linear, and constant forms you need. The assembler will automatically add up the contributions of all bilinear forms into a sparse matrix, those of all the linear forms into a vector, and those of all the constant forms into a scalar. To assemble multiple problems, simply form a ```list``` of all relevant problems.

```python
from assembler import Assembler
from assembler import Kernel
from assembler import Form

# Define weak form of elliptic problem
problem1 = [Form(eq, test=phix_1, trial=phix_1),
            Form(eq, test=phiy_1, trial=phiy_1),
            Form(1, test=phi_1)]

# Define integration operator
problem2 = [Form(1,test=phi_1,flag='dmn')]

# Put them together in a list
state = [problem1,problem2]

# Assemble system
assembler = Assembler(state)  # initialize the assembler
assembler.add_dirichlet('bnd', dir_fn=0, i_problem=0)  # add homogeneous Dirichlet conditions for problem 0
assembler.assemble()  # assemble

```

The vector representing the integration operator $\iint_R \phi_i dx$ can be obtained by

```python
J = assembler.get_vector(i_problem=1)
```

#### Solver

To solve the elliptic equation, you simply specify the problem (default=0), the matrix sample (default=0), and the vector sample for the right hand side (default=0) you want to use, i.e.

```python
# Solve system
u_vec = assembler.solve(i_problem=0, i_matrix=20)

# Define a finite element function using the solution vector
u = Nodal(basis=phi_1, data=u_vec)

# Plot
plot.contour(u)
```

#### Quantity of Interest

We can now compute $J = \iint_R u dx$ by taking dot product

```python
J_sample = J.dot(u_vec)

print(J_sample)
```

    0.0006180484835314365

## Multiresolution Approximation

```python

```


# Using analagous interface to qutips mesolve

```python
import qutip as q
from spylind import spylind as spl
import sympy as sm
import numpy as np
from matplotlib import pyplot as plt
%matplotlib inline
%load_ext autoreload
%autoreload 2
```

    No pretty printing stuff (probably because no Pandas)

# Qubit dynamics

## Single uniformly driven qubit

### Qutip version

```python
#def qubit_integrate(epsilon, delta, g1, g2, solver):
epsilon = 0.0 * 2 * np.pi   # cavity frequency
delta   = 1.0 * 2 * np.pi   # atom frequency
g2 = 0.15
g1 = 0.0

# intial state
psi0 = q.basis(2,0)
H = epsilon / 2.0 * q.sigmaz() + delta / 2.0 * q.sigmax()
# collapse operators
c_ops = []
if g1 > 0.0:
    c_ops.append(np.sqrt(g1) * q.sigmam())
if g2 > 0.0:
    c_ops.append(np.sqrt(g2) * q.sigmaz())

e_ops = [q.sigmax(), q.sigmay(), q.sigmaz()]
tlist = np.linspace(0,2,100)
output = q.mesolve(H, psi0, tlist, c_ops, e_ops)
```

```python
plt.plot(np.array(output.expect).T )
```

```python

#output = spl.mesolve([(sm.symbols('H_0'),H) ], psi0, tlist, c_ops, e_ops)
output = spl.mesolve([H] , psi0, tlist=tlist, c_ops = c_ops, e_ops=e_ops )
```

    makeMESymb enter
    Munch({'tSym': t, 'dimSyms': [], 'prop_state_syms': [\rho_{0|0}, \rho_{1|1}, \rho_{0|1}], 'stationary_state_syms': [], 'driving_syms': [], 'state_dep_syms': []})
    state dependent functions should have signature [t, \rho_{0|0}, \rho_{1|1}, \rho_{0|1}]
    Not integrating first step (it's just the initial state)

```python
plt.plot(tlist, output)
plt.legend()
```

### Inhomogeneously broadened ensemble of qubits

```python
output = spl.mesolve([H] , psi0, tlist=tlist, c_ops = c_ops, e_ops=e_ops )
```

    False

```python
psi0.data.todense()
```

    matrix([[1.+0.j],
            [0.+0.j]])

## Time dependent driving of qubit

```python
#def qubit_integrate(epsilon, delta, g1, g2, solver):
epsilon = 0.0 * 2 * np.pi   # cavity frequency
g2 = 0.15
g1 = 0.0

# intial state
psi0 = q.basis(2,0)
H0 = [epsilon / 2.0 * q.sigmaz(), lambda t,args: 1 ]
H1  = [q.sigmax(), lambda t, args: 5*np.sqrt(t)  ]

# collapse operators
c_ops = []
if g1 > 0.0:
    c_ops.append(np.sqrt(g1) * q.sigmam())
if g2 > 0.0:
    c_ops.append(np.sqrt(g2) * q.sigmaz())

e_ops = [q.sigmax(), q.sigmay(), q.sigmaz()]
tlist = np.linspace(0,4,200)
output = q.mesolve([H0,H1], psi0, tlist, c_ops, e_ops)
```

```python
plt.plot(np.array(output.expect).T)
```

```python
Esym = sm.symbols('E', real=True)
H0 = epsilon / 2.0 * q.sigmaz()
H1  = [Esym, q.sigmax()]
output = spl.mesolve([H0,H1], psi0, tlist, c_ops, e_ops, t_dep_fL = {Esym: lambda t: 5*np.sqrt(t)}, max_step_size=0.01)
```

    No pretty printing stuff (probably because no Pandas)
    Munch({'tSym': t, 'dimSyms': [], 'prop_state_syms': [\rho_{0|0}, \rho_{1|1}, \rho_{0|1}], 'stationary_state_syms': [], 'driving_syms': [E], 'state_dep_syms': []})
    state dependent functions should have signature [t, \rho_{0|0}, \rho_{1|1}, \rho_{0|1}, E]
    Not integrating first step (it's just the initial state)

```python
plt.plot(tlist, output)
plt.legend()
```

```python

```


# 微分の基礎

## 微分と関数最小化の関係

前章で微分が目的関数の最小化に役立つと紹介しました。本節ではまず具体例を用いてそのことを直感的に理解しましょう。
例として、下図のような下向きにくぼんだ形をした関数がどこで最小値をとるかを探す問題を考えます。

適当な点 $\theta_{1}$ でこの関数のグラフに接する直線（**接線**）を考えます（[注釈1](#note1)）。
仮にこの接線の傾きが $+3$ 、すなわち正の値であったとしましょう。この時、接線は右に進むほど高さが上がり，逆に左に進むほど高さが下がります。
$\theta_{1}$ の周辺では、関数のグラフと接線は非常に近く、両者はほとんど見分けることができません。
すると、関数も接線と同じように $\theta_{1}$ 右に進むと増加し，左に進むと減少していることがわかります。

次に、グラフ上の別の点 $\theta_{2}$ においてこのグラフに接する接線を考えます。
今度は接線の傾きが $-1$ であるとします。傾きが負のため、接線は右肩下がりの直線です。
グラフは接線と非常に近いため、グラフも $\theta_{2}$ の周辺では、同じように右肩下がりであるとわかります。

最後にちょうどグラフの谷となっている点 $\theta_{3}$ を考えます。
図を見て分かる通り、関数はこの点で最小値を取ります。
一方、点 $\theta_{3}$ でグラフに接する接線は水平、すなわち**傾きが0**です。

これまでの観察をまとめると、接線の傾きと関数の振る舞いには次の関係があることがわかります。

- ある点で接する接線の傾きが正ならば、その点の近くでグラフは右肩上がり（= 左に進むと高さが下がる）
- ある点で接する接線の傾きが負ならば、その点の近くでグラフは右肩下がり（= 右に進むと高さが下がる）
- 関数が最小値をとる点に接する接線の傾きは $0$ である

つまり、関数の最小値をとる点を求める問題では、接線の傾きが $0$ となる点が答えの候補（[注釈2](#note2)）となることがわかります。
本章で説明するように、**微分を用いると接線の傾きを計算することができます**。
このことから、微分が関数の最小化問題に有用なツールであることがわかります。

以降では、微分の定義と微分に関する公式を紹介します。さらに、入力が多変数の関数での微分（偏微分）についても解説します。

## 2 点間を通る直線の傾き

接線の傾きと微分の関係を調べるため、まずは2点を通る直線の傾きを求める問題を考えます。

直線の傾きは 「$y$ （縦方向）の増加量 / $x$ （横方向）の増加量」で計算できるので、上図の直線の傾き $a$ は、

$$
a = \dfrac{f(x_{2}) - f(x_{1})}{x_{2}-x_{1}}
$$

で求められます。

## 接線の傾き

上図での点 $x_1$ における接線の傾きを求めるために、もう一方の点 $x_2$ を $x_1$ に近づけていきます。ただし、 $x_1$ と $x_2$ が完全に同じになってしまうと、$x$ 方向、 $y$ 方向の増加量がどちらも $0$ になってしまうため、傾きは $0/0$ で計算することができません。
そこで 2 点は異なる点でありつつ、 $x_2$ を限りなく $x_1$ に近づけていった時、直線の傾きがどのように振る舞うかを見る必要があります。これを数式的に表現するには、**極限**の考えが必要になります。

極限では、変数がある値に限りなく近づくとき、その変数によって記述される関数がどのような値に近づくかを考えます。
関数 $f$ に対し、$h$ という変数を $a$ に近づけていったときの $f(h)$ が近づく値を $\lim$ という記号を用いて

$$
\displaystyle \lim _{h\rightarrow a} f(h)
$$

と書きます。例えば、 $h$ を限りなく $0$ に近づけた時に、 $3h$ も限りなく $0$ に近づいていきます。
従って

$$
\displaystyle \lim _{h\rightarrow 0} 3h=0
$$

です。もっと一般に $n$ を自然数、 $c$ を定数として、

$$
\displaystyle \lim _{h\rightarrow a} c h^n=c a^n
$$

が成立します。これだけ見ると単に関数 $\lim _{h\rightarrow a} f(h)$ は $h$ に $a$ を代入した値 $f(a)$ のように思えるかも知れません。しかし、今考えている直線の傾きの場合、そのような代入操作を行うと先程のように $0/0$ の形が現れてしまうため。単純な代入では極限を求めることはできません。
極限の計算方法は後ほど詳しく解説します。

それでは、下図のある点 $x$ における接線の傾き $a$ を求めていきましょう。

2点を通る直線の傾きの式と極限を組み合わせて、接線の傾きを求めることができます。

はじめに、 $x$ から $h$ だけ離れた点 $x + h$ を考え、2点を通る直線の傾きを求めてみます。
次に $h$ を $0$ に限りなく近づけていけば、1点 $x$ で接する接線を考えることができます。
これを式でみると

$$
\begin{aligned}
a
&= \lim_{h \rightarrow 0} \frac{f(x + h) - f(x)}{(x + h) - x} \\
&= \lim_{h \rightarrow 0} \frac{f(x + h) - f(x)}{h} \\
\end{aligned}
$$

となります。よく見るとこの式は $x$ を決めるごとにある1つの値を定めています、すなわちこの式は $x$ の関数です（$h$ は $h\to 0$ の極限を取っているので $h$ の関数にはなっていないことに注意してください）。
この式を $f$ の **導関数 (derivative)**と呼び、 $f'(x)$ と書きます。すなわち、

$$
f'(x)= \lim_{h \rightarrow 0} \frac{f(x + h) - f(x)}{h}
$$

です。導関数を求めることを**微分(differentiation)**するといいます。
記号の使い方として、 $f'(x)$ を

$$
\frac{d}{dx}f (x)
$$
または
$$
\frac{df}{dx} (x)
$$

と書いても構いません。
この $d$ という記号は増分をとる操作を表しており、例えば $dx$ は $x$ の変化量を表します。
この記法は煩雑ですが、$x, y$ など変数が複数ある場合、 どの変数で微分しているかが明確になるため、表現を正確にすることができます。

## 微分の公式

ある関数の導関数を計算するには、導関数の定義通りに計算するのが最も愚直な方法です．
しかし、これから紹介する種々の公式を組み合わせることで、わざわざ定義に戻らなくても、複雑な関数の導関数を計算できます。
以下では、 $c$ は定数、 $x$ は変数を表します。

まずはじめに、以下の 3 つの公式を紹介します。

$$
\begin{align}
\left( c\right)^{'} &= 0 \\
\left( x\right)^{'} &= 1\\
\left( x^{2}\right)^{'} &= 2x
\end{align}
$$

1つ目の左辺は定数関数 $f(x) = c$ を $x$ で微分した導関数を表しています。
定数関数のグラフを書くと $x$ 軸に平行になるため、どの点で接線を引いてもその傾きは $0$です（グラフと接線は同一の直線となります）。これは導関数は 0 であることを意味します。これが 1 つ目の式の図形的な理解です。
同様に2つ目の公式は 「$y=x$ のグラフではどの点で接線を引いても傾きが1」であることを意味します（この場合もグラフと接線は同一の直線です）。
極限の扱いに慣れるために、3番目の公式を証明しましょう。導関数の定義から左辺は

$$
\displaystyle \lim _{h\to 0} \frac{(x+h)^2 - x^2}{(x+h) - x}
$$
です。これは

$$
\begin{align}
\lim _{h\to 0} \frac{(x+h)^2 - x^2}{(x+h) - x} = \lim _{h\to 0} \frac{2xh + h^2}{h} = \lim _{h\to 0} (2x + h)
\end{align}
$$

と計算できます。ここで $h\to 0$ 、すなわち $h$ を限りなく $0$ に近づけると、 $2x + h$ は $2x$ に限りなく近づいていきます。従って、 $\left( x^{2}\right)^{'} = 2x$ が導かれました。

以下の公式も頻繁に利用します。

$$
\begin{align}
\left( x^{n} \right)^{'} &= nx^{n-1} \\
\left( e^{ax} \right)^{'} &= ae^{ax}
\end{align}
$$

ここで、$a$ は定数、 $e$ は**自然対数の底**、もしくは **ネイピア数** と呼ばれる特別な定数で、およそ $2.71828\cdots$ です。 $1$ つ目の公式で $n=1, 2$とすると、本節の最初に紹介した $x$, $x^2$ の微分の公式に帰着されることに注意してください。
$e^{ax}$ は $\exp(ax)$ のように表記されることがよくあります。その表記を用いると最後の公式は

$$
\bigl( \exp(ax) \bigr)^{'} = a\exp(ax)
$$

とも書けます。

## 線形性

微分は**線形性**という性質を持っています。
それがどのような性質なのか、具体例を挙げて見ていきましょう。
微分には線形性という性質によって、

$$
(3x)' = 3 \times (x)'
$$

のように定数項を微分の演算の外側に出すことができます。
また、

$$
\left( 3x^{2} + 4x - 5 \right)' = \left( 3x^{2} \right)' + \left( 4x \right)' - \left( 5 \right)'
$$

のように、加算や減算はそれぞれ項ごとに独立に微分の演算を行うことができます。
この 2 つの特性を合わせて線形性と呼びます。

もう少し微分の計算を練習してみましょう。

$$
\begin{aligned}
\left( 3x^{2} + 4x + 5 \right)' &= \left( 3x^{2} \right)' + \left( 4x \right)' - \left( 5 \right)' \\
&= 3 \times \left( x^{2} \right)' + 4 \times \left( x \right)' - 5 \times \left( 1 \right)' \\
&= 3 \times 2x + 4 \times 1 - 5 \times 0  \\
&= 6x + 4
\end{aligned}
$$

この線形性に関しては、下記のように公式としてまとめることができます。

$$
\begin{align}
\left( cf(x) \right)^{'} &= c f'(x) \\
\left( f(x) + g(x) \right)^{'} &= f^{'}(x) + g^{'}(x) \\
\end{align}
$$

2 つの関数の積の形で書かれている関数に関しては次の公式が成り立ちます。

$$
\bigl( f(x) g(x) \bigr)^{'} = f^{'}(x)g(x) + f(x)g^{'}(x)
$$

関数 $f$ の導関数と関数 $g$ の導関数がわかれば、関数 $fg$ を計算できることがわかります。

## 合成関数の微分

関数 $y = f(x)$ と $z = g(y)$ の**合成**とは $f$ を適用したあとに $g$ を適用する関数、すなわち $z  = g(f(x))$ のことを指します。
ディープラーニングで用いるニューラルネットワークは、層を何層も重ねて複雑な関数を表現します。各々の層を 1 つの関数とみなすと、ニューラルネットワークは多くの関数（層）を合成した**合成関数**と見ることができます。
合成関数の微分を考える時には次に紹介する公式（合成関数の微分の公式）が有用です。
この公式は**連鎖律 (chain rule)** とも呼ばれています。
連鎖律は合成関数を微分を簡単に計算するための公式というだけではなく、ニューラルネットワークの訓練方法である誤差逆伝播法を理解する上で本質的な役割を果たします。

簡単な例として、

$$
\left\{ (3x + 4)^{2} \right\}'
$$

を計算することを考えます。
この式は、 $3x+4$ という内側の部分と $(\cdot)^{2}$ という外側の部分で構成されています。
この式を $(9x^2 + 24x + 16)'$ のように展開してから微分を計算しても良いのですが、3乗や4乗とべき数が増えると式を展開するのが大変になります。
ここで役に立つ考え方が合成関数の微分です。
合成関数の微分は、内側の微分と外側の微分をそれぞれ行い、その結果をかけ合わせることで求めることができます。
外側の微分の際には関数の引数を入力とみなし、その入力についての微分を計算します。

それでは、具体的にこの $(3x+4)^2$ という関数の微分を考えてみます。
まず内側の関数を $u = (3x+4)$ とおいて、

$$
\left\{ (3x + 4)^{2} \right\}' = (u^{2})'
$$

と見ます。ここで、 $(\cdot)'$ をもう少し厳密に考える必要が出てきます。
今変数は $x$, $u$ の2つありるため、 $(\cdot)'$ という表記では、 $x$ で微分しているのか $u$ で微分しているのかの区別がつきません。
そこで、多少複雑に見えますが、先程紹介した $d$ を使った記法を用いて微分する変数を明示します。

合成関数の微分を公式としてまとめると次のようになります。

$$
\frac{d}{dx} f(g(x)) = \frac{df(u)}{du}\frac{du}{dx}
$$

ここで $u = g(x)$ です。

公式を見るよりも実際の適用例を見た方が理解しやすいかも知れません。
合成関数の微分の公式を用いて、先程の $(3x+4)^2$ の微分を計算すると次のようになります。
2行目で合成関数の微分の公式を利用していることに注目してください。

$$
\begin{aligned}
\left\{ (3x + 4)^{2} \right\}' &= \frac{d}{dx} \left\{ (3x + 4)^{2} \right\} \\
&= \frac{du}{dx} \frac{d}{du} (u^2) \\
&= \frac{d}{dx} (3x + 4) \cdot \frac{d}{du} (u^{2}) \\
&= 3 \cdot 2u   \\
&= 6u = 6(3x + 4) = 18x + 24 \\
\end{aligned}
$$

気になる人は、 $(3x + 4)^2$ を展開してから各項を微分した場合と結果が一致していることを確かめてみてください。

## 偏微分

機械学習では、1つの入力変数 $x$ から出力変数 $y$ を予測するケースは稀であり、多くの場合、複数の入力変数 $x_1, x_2, \dots, x_M$ を用いて $y$ を予測する**多変数関数**が扱われます。
例えば、家賃を予測する場合、部屋の広さだけではなく、駅からの距離や周辺の犯罪発生率なども同時に考慮した方がより正確に予測ができると期待されます。
複数の入力 $x_1, x_2, \dots, x_M$ をとる関数 $f(x_1, x_2, \dots, x_M)$ を多変数関数とよびます。
この多変数関数において、ある入力 $x_m$ にのみ注目して微分することを **偏微分** とよび、

$$
\frac{\partial}{\partial x_{m}} f(x_1, x_2, \dots, x_M)
$$

と表します。微分を意味する記号が、 $d$ から $\partial$ に変わっています。こうすると、 $\frac{\partial}{\partial x_m}$ は $x_m$ 以外を定数と考え、 $x_m$ にのみ着目して微分を行うという意味となります（[注釈3](#note3)）。

以下の例で具体的な計算の流れを確認しましょう。

$$
\begin{aligned}
\frac{\partial}{\partial x_1}
\left( 3x_1+4x_2 \right)
&= \frac{\partial}{\partial x_1}
\left( 3x_1 \right) + \frac{\partial}{\partial x_1} \left( 4x_2 \right) \\
&= 3 \times \frac{\partial}{\partial x_1} \left( x_1 \right) + 4 \times \frac{\partial}{\partial x_1} x_2 \\
&= 3 \times 1 + 4 \times 0 \\
&= 3
\end{aligned}
$$

偏微分でも微分と同じ公式を適用できます。今回のケースでは、 $x_1$ にだけ着目しており、 $x_2$ は定数として扱かっています。そのため、上式の 2 行目から 3 行目で $x_2$ を $x_1$ で偏微分した値を $0$ としています（定数の微分は $0$ であったことを思い出してください）。

**注釈 1**

ここで考えている関数のグラフでは、グラフ上のどの点を取ってもその点で接する接線がただ1本だけ引ける状況を考えています。例えば関数のグラフが谷の部分で「尖った」形をしていると，谷の底で複数の接線が引けてしまいます。ここではそのようなケースは考えず、関数のグラフは図のような「滑らか」なカーブになっている場合をイメージしてください。

[▲上へ戻る](#ref_note1)

**注釈 2**

今は関数のグラフの「谷」を考えましたが、「山」でも同様に接線の傾きが $0$ となるため、ある点での接線の傾きが $0$ だからと言って、必ずしも関数がその点で最小値をとるとは限りません。

[▲上へ戻る](#ref_note2)

**注釈 3**

入力変数が他の入力変数と独立でない場合は定数と考えることはできません。しかし本資料ではそのようなケースは出てきません。

[▲上へ戻る](#ref_note3)


Table of Contents

1  Introduction

1.1  Simulation

1.1.1  The Mesh

1.1.2  The Elements and DofHandlers

1.1.3  The Gaussian Field

1.1.4  Assembly

1.1.5  Solver

1.1.6  Quantity of Interest

2  Multiresolution Approximation

# Optimal Upscaling

```python
# Add src folder to path
import os
import sys

sys.path.insert(0,'../../src/')
```

## Introduction

Suppose $u$ satisfies the elliptic partial differential equation

\begin{align*}
-\nabla \cdot (\exp(q) \nabla u) = 1, \ \ &x \in D,\\
u = 0, \ \ &x \in \partial D
\end{align*}

where $D=[0,1]^2$ is a physical domain and $q(x,\omega)$ is a Gaussian random field. Moreover, let

\begin{equation}
J = \iint_{R}  u(x,\omega) dx, \ \ \text{where } R = [0.75,1]^2,
\end{equation}

be a physical quantity of interest for which we want to compute statistics using Monte Carlo sampling or whatever.

### Simulation

First, we show how to generate a single sample of $J$. Specifically, we will

1. Construct the computational mesh and define the elements
2. Generate samples of the random parameter $q$
3. Assemble the finite element system
4. Solve the system for $u$
5. Compute the associated $J$

We will use the ```quadmesh``` package, obtainable [here](https://github.com/hvanwyk/quadmesh).

#### The Mesh

The ```mesh``` module contains methods for generating and working with meshes. In this work, we use quadrilateral meshes, since they can be readily refined. On our mesh, we need to mark the boundary $\partial D$ and the region of integration $R$. To do this, we use marking functions that check whether a point lies in the given region.

```python
from mesh import QuadMesh
from plot import Plot

plot = Plot()

# Define the Quadrilateral Mesh
mesh = QuadMesh(resolution=(16,16))

# Mark boundary
bnd_fn = lambda x,y: abs(x)<1e-6 or abs(1-x)<1e-6 or abs(y)<1e-6 or abs(1-y)<1e-6
mesh.mark_region('bnd', bnd_fn, entity_type='half_edge', on_boundary=True)

# Mark averaging region
dmn_fn = lambda x,y: x>=0.75 and x<=1 and y>=0.75 and y<=1
mesh.mark_region('dmn', dmn_fn, entity_type='cell', strict_containment=True, on_boundary=False)
#cells = mesh.get_region(flag='dmn', entity_type='cell', on_boundary=False, subforest_flag=None)
plot.mesh(mesh, regions=[('bnd','edge'),('dmn','cell')])
```

#### The Elements and DofHandlers

Next we need to define the approximation space, i.e. the elements. This is done using the ```QuadFE``` class.
- Here we use piecewise constant elements (```DQ0```) to represent the parameter and piecewise linear elements (```Q1```) for the solution.
- Since we would like to have multiple different elements associated with the same mesh, we handle the element degrees of freedom by means of a **degrees-of-freedom-handler** or ```DofHandler```, which contains information of the ```mesh``` and the ```element```.
- For the assembly and function definitions, we may also require ```Basis``` functions. These also encode derivative information.

```python
from fem import QuadFE, DofHandler, Basis

#
# Elements
#
Q0 = QuadFE(mesh.dim(), 'DQ0')  # "Discontinuous Quadratic of Degree 0", for parameter
Q1 = QuadFE(mesh.dim(), 'Q1')  # "Continuous Linear" for output

#
# DofHandlers
#
dQ0 = DofHandler(mesh,Q0)
dQ1 = DofHandler(mesh,Q1)

#
# Distribute DOFs, i.e. store the dof-indices in the dofhandler
#
dQ0.distribute_dofs()
dQ1.distribute_dofs()

#
# Basis functions for assembly and definition of Nodal functions
#
phi_0 = Basis(dQ0)  # piecewise constant

phi_1 = Basis(dQ1)  # piecewise linear
phix_1 = Basis(dQ1,'ux')  # piecewise linear first partial w.r.t. x
phiy_1 = Basis(dQ1,'uy')  # piecewise linear first partial w.r.t. y
```

#### The Gaussian Field

The ```gmrf``` module contains routines to generate a variety of Gaussian random fields.
1. First, the covariance matrix is assembled from a bivariate kernel function using the ```Covariance``` class. The assembly is dependent on the mesh and approximation space (element) associated with the field.
2. Next we compute its eigendecomposition.
3. Finally we define a Gaussian random field using the ```GaussianField``` class, which allows us to sample, condition, etc.
4. Samples from the Gaussian field are given as vectors but we can use these to define ```Nodal``` interpolants on the given mesh.

```python
from function import Nodal
from gmrf import Covariance
from gmrf import GaussianField
import numpy as np

#
# Approximate Covariance Matrix
#
cov = Covariance(dQ0, name='gaussian', parameters={'l':0.01})
cov.compute_eig_decomp()

#
# Define the Gaussian field
#
q = GaussianField(dQ0.n_dofs(), K=cov)

# Sample Random field and store all samples in a Nodal DQ0 finite element function
n_samples = 100
eq = Nodal(basis=phi_0, data=np.exp(q.sample(n_samples)))

#
# Plot a single sample
#
plot.contour(eq, n_sample=20)
```

#### Assembly

The weak form of the elliptic PDE is given by

\begin{equation}\label{eq:weak_form}
\iint_D \exp(q) \nabla u \cdot \nabla \phi dx = \iint_D 1 \phi dx,
\end{equation}

for any test function $\phi$. Approximating $u = \sum_j c_j \phi_j$ in the Galerkin framework, we get $A c = b $, where

\begin{equation}\label{eq:bilinear}
A_{ij} = \iint_D \exp(q) \nabla \phi_j \cdot \nabla \phi_i dx = \iint_D \exp(q) \frac{\partial \phi_j}{\partial x} \frac{\partial \phi_i}{\partial x} + \exp(q)  \frac{\partial \phi_j}{\partial u}\frac{\partial \phi_i}{\partial y} dx
\end{equation}

and

\begin{equation}\label{eq:linear}
b_i = \iint_D \phi_i dx.
\end{equation}

The assembly therefore requires a bilinear form $A$ and a linear form $b$. We define each form with the ```Form``` class in the ```assembler``` module.

Each form requires a ```Kernel``` function, possibly a ```test``` function, and possibly a ```trial``` function (both of which should be ```Basis``` functions).

> For example, the linear form \ref{eq:linear} is defined by ```Form(1, test=phi_1)```, while the bilinear form $\iint_D \exp(q) \frac{\partial \phi_j}{\partial x} \frac{\partial \phi_i}{\partial x}dx$ is stored as ```Form(eq, test=phix_1, trial=phix_1)```.

1. The object ```Form(kernel)``` is assembled into a scalar.
2. The linear form ```Form(kernel, test=phi_1)``` is assembled into a vector.
3. The bilinear form ```Form(kernel, test=phix_1, trial=phix_1)``` is assembled into a sparse matrix.

Some forms, such as $\iint_R \phi_i dx$ should only be assembled over a subdomain of the mesh. To do this, use ```Form(1, test=phi_1, flag='dmn')```, where ```dmn``` was the flag marking the region $R$ from earlier.

> __REMARK:__ When the kernel is a function with multiple samples (such as our function ```eq```), then the relevant form is assembled for all samples.

The ```Assembler``` class handles finite element assembly. For each problem you want to assemble, e.g. the elliptic problem, or the problem of computing $J$, you list all relevant bilinear, linear, and constant forms you need. The assembler will automatically add up the contributions of all bilinear forms into a sparse matrix, those of all the linear forms into a vector, and those of all the constant forms into a scalar. To assemble multiple problems, simply form a ```list``` of all relevant problems.

```python
from assembler import Assembler
from assembler import Kernel
from assembler import Form

# Define weak form of elliptic problem
problem1 = [Form(eq, test=phix_1, trial=phix_1),
            Form(eq, test=phiy_1, trial=phiy_1),
            Form(1, test=phi_1)]

# Define integration operator
problem2 = [Form(1,test=phi_1,flag='dmn')]

# Put them together in a list
state = [problem1,problem2]

# Assemble system
assembler = Assembler(state)  # initialize the assembler
assembler.add_dirichlet('bnd', dir_fn=0, i_problem=0)  # add homogeneous Dirichlet conditions for problem 0
assembler.assemble()  # assemble

```

The vector representing the integration operator $\iint_R \phi_i dx$ can be obtained by

```python
J = assembler.get_vector(i_problem=1)
```

#### Solver

To solve the elliptic equation, you simply specify the problem (default=0), the matrix sample (default=0), and the vector sample for the right hand side (default=0) you want to use, i.e.

```python
# Solve system
u_vec = assembler.solve(i_problem=0, i_matrix=20)

# Define a finite element function using the solution vector
u = Nodal(basis=phi_1, data=u_vec)

# Plot
plot.contour(u)
```

#### Quantity of Interest

We can now compute $J = \iint_R u dx$ by taking dot product

```python
J_sample = J.dot(u_vec)

print(J_sample)
```

    0.0009451245270289313

## Multiresolution Approximation

We would like to represent our spatially varying input parameter $q$ in a mult-resolution decomposition. Specifically, if $\tilde{\mathbb{T}}$ is a sub-mesh of $\mathbb{T}$ we would like to construct the projection of our piecewise constant function $q$ onto subspace spanned by the piecewise constant basis functions on the coarse mesh $\tilde{\mathbb{T}}$. To this end, we define a projection matrix.

```python
from scipy import sparse as sp

def projection_matrix(dofhandler, fine_flag, coarse_flag):
    """
    Project a piecewise constant function, defined on a fine scale mesh onto
    a coarse scale mesh.

    Inputs:

        dofhandler: DofHandler, for discontinuous piecewise constant elements

        fine_flag: str/int, mesh-flag for fine mesh

        coarse_flag: str/int, mesh-flag for coarse mesh

    Outputs:

        P: double, sparse (n_dofs_coars, n_dofs_fine) matrix representation of
            the projection,

    """
    assert dofhandler.element.element_type()=='DQ0', \
        'Only piecewise constant approximations supported.'

    mesh = dofhandler.mesh
    rows, cols, vals = [], [], []
    for leaf in mesh.cells.get_leaves(subforest_flag=fine_flag):
        # Iterate over fine mesh

        # Add leaf dof to list of columns
        cols.extend(dofhandler.get_cell_dofs(leaf))

        # Search for nearest ancestor in coarse grid
        ancestor = leaf
        while not ancestor.is_marked(coarse_flag):
            ancestor = ancestor.get_parent()

        # Record coarse cell dof
        rows.extend(dofhandler.get_cell_dofs(ancestor))

        # Determine the ratio in areas
        if mesh.dim()==1:
            # One-dimensional interval
            multiplier = leaf.length()/ancestor.length()
        elif mesh.dim()==2:
            # Two-dimensional cell
            multiplier = leaf.area()/ancestor.area()

        # Store the value
        vals.append(multiplier)

    #
    # Re-index rows and columns
    #

    # Compute unique dofs
    col_dofs = list(set(cols))
    row_dofs = list(set(rows))

    # Re-index using unique dofs
    rows = [row_dofs.index(i) for i in rows]
    cols = [col_dofs.index(i) for i in cols]

    #
    # Define sparse projection matrix
    #
    n_rows = len(row_dofs)
    n_cols = len(col_dofs)

    P = sp.coo_matrix((vals,(rows,cols)), shape=(n_rows,n_cols))

    return P

```

Let's test it out. Take $q(x_1,x_2) = \sin(\pi x_1)\sin(\pi x_2)$ on $[0,1]^2$ and project it onto a lower dimensional mesh.

```python
import matplotlib.pyplot as plt

# Define initial coarse mesh
mesh = QuadMesh(resolution=(4,4))

# Label the coarse mesh
mesh.record('coarse')

# Refine the mesh
for i in range(3):
    mesh.cells.refine()

# Label the fine mesh
mesh.record('fine')

# Plot the meshes
fig, axes = plt.subplots(1,2,figsize=(8,4))
plot = Plot(quickview=False)
axes[0] = plot.mesh(mesh, axis=axes[0], subforest_flag='coarse')
axes[0].set_title('Coarse Mesh')

axes[1] = plot.mesh(mesh, axis=axes[1], subforest_flag='fine')
axes[1].set_title('Fine Mesh')

plt.show()
```

```python
#
# Define Piecewise Constant Elements
#
Q0 = QuadFE(2,'DQ0')

# DofHandler
dQ0 = DofHandler(mesh,Q0)
dQ0.distribute_dofs()

# Basis on the fine mesh
phi_fine = Basis(dQ0)

# Define function on the fine mesh (note how x is treated as a two column array)
q_fine = Nodal(f=lambda x: np.sin(np.pi*x[:,0])*np.sin(np.pi*x[:,1]), basis=phi_fine)

# Compute the projection matrix
P = projection_matrix(dQ0,'fine','coarse')

# Basis function on the coarse mesh
phi_coarse = Basis(dQ0, subforest_flag='coarse')

# Define the projection of q onto the coarse basis
q_coarse = Nodal(data = P.dot(q_fine.data()), basis=phi_coarse)

#
# Plot Results
#
fig, axes = plt.subplots(1,2, figsize=(12,5))

axes[0] = plot.contour(q_fine,axis=axes[0])
axes[0].set_title('Original Function')

axes[1] = plot.contour(q_coarse, axis=axes[1])
axes[1].set_title('Projection')

plt.show()
```


# Teoría del Error

Python en Jupyter Notebook

Creado por Giancarlo Ortiz para el curso de Métodos Numéricos

## Acumulación
En los métodos numéricos y en general en investigación científica es de mucha importancia conocer el efecto acumulado de los errores en la incertidumbre de una función matemática.

## Agenda
1. Propagación
1. Método iterativo
1. Convergencia
1. Estabilidad

```python
# Importar módulos al cuaderno de jupyter
import math as m
import numpy as np
import pylab as plt

# Definir e incluir nuevas funciones al cuaderno
def _significativas(valor, cifras):
    ''' Reducir un valor a un numero de cifras significativas. '''
    Primera_significativa = -int(m.floor(m.log10(abs(valor))))
    decimales = Primera_significativa + cifras - 1
    return round(valor, decimales)

def _normalizar(valor, referencia):
    ''' Aproximar un numero a las cifras significativas de la referencia. '''
    Primera_significativa = -int(m.floor(m.log10(abs(referencia))))
    cifras = Primera_significativa + 1
    return _significativas(valor, cifras)

def _zeros(a, b, c):
    ''' Obtener las dos raíces de la ecuación cuadrática. '''
    t1 = (- b + (b**2-4*a*c)**(1/2)) / (2*a)
    t2 = (- b - (b**2-4*a*c)**(1/2)) / (2*a)
    return t1, t2
```

## 1. Propagación
---
En matemáticas la propagación del error es el efecto de variación en la incertidumbre de la variable dependiente dada una incertidumbre en alguna de las variables independientes. A continuación, se define el problema para una función lineal y de n variables independientes.

\begin{align}
y &= f \left( x_1 \pm \Delta x_1, x_2 \pm \Delta x_2, ... , x_n \pm \Delta x_n \right) \\
\Delta y &= \left| \frac{\partial f}{\partial x}\right| \Delta x + \left| \frac{\partial f}{\partial y}\right| \Delta y + ... + \left| \frac{\partial f}{\partial y}\right| \Delta y
\end{align}

>**NOTA:** Típicamente estas funciones en investigación científica son complejas y no son lineales; por lo que se requieren técnicas de linealización aproximada, usando para ello instrumentos matemáticos como la expansión de términos en series.

### Ejemplo: Modelo de gravitación básico
---
Cálculo del error de propagación en la gravedad, calculada como un valor aparente $\color{#a78a4d}{g_a}$ a partir de un conjunto de medidas del periodo de oscilación en un sistema de péndulo simple con longitud conocida.

\begin{align}
To & = 2 \pi \sqrt{\frac{ L}{g_a}} \\
g_a(To) & = 4\pi^2 L\frac{1}{T_o^2} \\
g_a(To) & = 4\pi^2 L T_o^{-2} \\
\Delta g_a & = \left| \frac{\partial g_a}{\partial T_o}\right| \Delta T_o \\
\frac{\partial g_a}{\partial T_o} & = -2 (4\pi^2 L) T_o^{-3}\\
\end{align}

```python
# Modelo de gravitación Básico
# Medidas directas de longitud y ángulo en el experimento
Longitud = 1; θ = 45; deltaT = 0.30

# Medidas directas del periodo cada 30 segundos
Tr = [2.106, 2.101, 2.098, 2.087, 2.073, 2.070, 2.064, 2.059, 2.057, 2.052]

# Valores reales
Lat = 1.209673                                                      # Latitud de (Pasto - Nariño)
Alt = 2_539                                                         # Altitud de (Pasto - Nariño)
R = 6_371_000                                                       # Radio medio de la tierra
Afc = 1 + 0.0053024 * m.sin(Lat)**2 - 0.0000058 * m.sin(2*Lat)**2   # Aporte de la fuerza centrifuga
Afg = 1 - (2/R)*Alt + (3/R**2)*Alt**2                               # Aporte de la distancia al centro
g = 9.780327 * Afc * Afg

# Péndulo Simple
Ti = np.array(Tr)               # Periodo (Medida directa)
θ_rad = θ * (m.pi/180)          # Conversión del ángulo a radianes (~Medida directa)
To = Ti / (1 + (θ_rad/4)**2)    # Corrección para ángulos grandes (~Medida directa)
K = 4 * Longitud * m.pi**2      # Constante de proporcionalidad (~Medida directa)

# Medida indirecta o aparente de la gravedad
ga = K / To**2

# Valores periodo
μT = sum(To) / len(To)                      # Valor promedio
DT = (sum((μT - To)**2) / len(To))**(1/2)   # Desviación estándar
ΔT = _significativas(DT, 1)                 # Incertidumbre del periodo (calculada de los datos)
μT_n = _normalizar(μT, ΔT)                  # Valores medios normalizados

# Valores gravedad aparente
μg = sum(ga) / len(ga)                                  # Valor promedio
Dg = (sum((μg - ga)**2) / len(ga))**(1/2)               # Desviación estándar
dga_dTo = -2 * (4 * Longitud * m.pi**2) * (μT_n)**(-3)  # Derivada
dg = abs(dga_dTo) * ΔT                                  # Calculo de la propagación del Error
Δg = _significativas(dg, 1)                             # Incertidumbre de la gravedad aparente
μg_n = _normalizar(μg, Δg)                              # Valores medios normalizados

# Salida estándar
print(f"-----------------------------------------------")
print(f"Valor promedio de T:           {μT:8.4f}")
print(f"Incertidumbre del grupo T:     {DT:8.4f}")
print(f"Valor aparente del grupo:      {μT_n:8.4f} ± {ΔT}")
print(f"-----------------------------------------------")
print(f"Valor promedio de g:           {μg:8.4f}")
print(f"Incertidumbre del grupo g:     {Dg:8.4f}")
print(f"Incertidumbre propagada:       {dg:8.4f}")
print(f"Valor aparente del grupo:      {μg_n:8.4f} ± {Δg}")
print(f"-----------------------------------------------")
print(f"Valor del modelo algebraico:   {g:8.4f}")
print(f"-----------------------------------------------")

```

    -----------------------------------------------
    Valor promedio de T:             1.9996
    Incertidumbre del grupo T:       0.0181
    Valor aparente del grupo:        2.0000 ± 0.02
    -----------------------------------------------
    Valor promedio de g:             9.8759
    Incertidumbre del grupo g:       0.1782
    Incertidumbre propagada:         0.1974
    Valor aparente del grupo:        9.9000 ± 0.2
    -----------------------------------------------
    Valor del modelo algebraico:     9.8179
    -----------------------------------------------

## 2. Método iterativo
---
Un método iterativo es un algoritmo que trata de resolver un problema matemático; como una ecuación o un sistema de ecuaciones, mediante aproximaciones sucesivas; empezando desde una estimación inicial o semilla $\color{#a78a4d}{k_o}$; de forma que cada valor $\color{#a78a4d}{k_n}$ es función del valor de prueba anterior.

\begin{align}
x_n & = x \left( k_n \right) \\
k_n & = f \left( x_{n-1} \right), \ \ (k>1) \\
\end{align}

>**NOTA:** Estos métodos son útiles para resolver problemas en sistemas complejos; que involucran un número muy grande de variables y comportamientos no lineales; que tendrían un [costo computacional](https://es.wikipedia.org/wiki/Teor%C3%ADa_de_la_complejidad_computacional) prohibitivo empleando métodos directos.

### Ejemplo: Movimiento Parabólico
---
Cálculo de la distancia maxima que recorre un objeto en movimiento parabólico, que parte de una altura $\color{#a78a4d}{h_o}$, con una velocidad inicial $\color{#a78a4d}{v_o}$ y un angulo de partida $\color{#a78a4d}{\theta_o}$.

```python
# [Definición del problema]
# Ecuación de la altura para el movimiento parabólico
def altura(θo, Vo, ho, t):
    g = 9.8179
    Voy = Vo * np.sin(θo*np.pi/180)
    y = -(1/2) * g * t**2 + Voy * t + ho
    return y

# Intervalos
Angulo_partida = 45
Velocidad_inicial = 12
altura_inicial = 3
ti = 0
tf = 3

# Renombrar la función
F = lambda t: altura(45, 12, 3, t)
```

```python
# [Solución algebraica]
""" Uno problema sencillo es la evaluación de una función en un punto dado.
La solución algebraica en este caso es posible a un bajo costo computacional,
pero es util para ejemplificar el concepto de método iterativo. """
# Ecuación de tiempo de vuelo para el movimiento parabólico
def tiempo(θ, Vo, ho):
    g = 9.8179
    Voy = Vo * np.sin(θ*np.pi/180)
    t = abs(_zeros(-g/2, Voy, ho)[1])
    return t

# [Solución gráfica]
t = np.linspace(ti, tf, 1024, endpoint=True)
S = altura(Angulo_partida, Velocidad_inicial, altura_inicial, t)
t_ho = tiempo(Angulo_partida, Velocidad_inicial, altura_inicial)
plt.figure(figsize=(16, 4))
plt.axhline(y = 0, linewidth=1.0, color="#ccc")
plt.axvline(x = 0, linewidth=1.0, color="#ccc")
plt.axvline(x = t_ho, linewidth=1.0, color="#ccc")
plt.plot(t, S, color="#F00")

# Textos en la gráfica
plt.title("Movimiento Parabólico\n".upper() +
         f"V = {Velocidad_inicial} m/s", fontsize=16, fontweight="bold")
plt.xlabel("Tiempo [s]", fontweight="bold")
plt.ylabel("Altura [m]", fontweight="bold")
plt.text(t_ho, 0, f" -> tiempo: {t_ho}")
plt.show()
```

```python
# [Acotar el intervalo de búsqueda]
""" Encontrar dos enteros consecutivos en donde ocurre un cambio de signo. """

# Ciclo while
""" este bucle evalúa una condición y si es verdadera, ejecuta el bloque de código interno.
Continúa evaluando y ejecutando mientras la condición sea verdadera. """
i = 0 # Variable para contar iteraciones
while True:
    if F(i) * F(i+1) < 0:
        break # Termina el ciclo
    else:
        i += 1

# Salida
Kmin = i
Kmax = i+1
Fi = F(Kmin)
Fs = F(Kmax)

# Salida
print(f"---------------------------------")
print(f"|      INTERVALO ENCONTRADO     |")
print(f"|     Iteraciones = {i}           |")
print(f"---------------------------------")
print(f"| Punto mínimo  ->  ({Kmin},{Fi:7.2f}) |")
print(f"| Punto máximo  ->  ({Kmax},{Fs:7.2f}) |")
print(f"---------------------------------")

```

    ---------------------------------
    |      INTERVALO ENCONTRADO     |
    |     Iteraciones = 2           |
    ---------------------------------
    | Punto mínimo  ->  (2,   0.33) |
    | Punto máximo  ->  (3, -15.72) |
    ---------------------------------

```python
# Defino el método iterativo
def iterativo(Xmin, Xmax, Imax):
    """ Método para encontrar raíces en un intervalo.

        ## Parámetros:
            Xmin (int): limite inferior de intervalo.
            Xmax (int): limite superior de intervalo.
            Imax (int): número máximo de iteraciones.

        ## Devoluciones:
            Km (float): valor de x encontrado.
            No (int)  : iteraciones.
            Tb (str): tabla con evoluvión de iteraciones.
    """
    # Inicializar variables
    Xi = Xmin
    Xs = Xmax
    Tb = f"| I # | {'Xi':>8} | {'Xs':>8} | {'Xm':>9} | {'Fm':>9} |\n"
    Tb += "-----------------------------------------------------\n"
    # Iteraciones
    for No in range(1, Imax + 1):
        Km = (Xs + Xi) / 2
        Fm = F(Km)
        Tb += f"| {No:3} | {Xi:8.5f} | {Xs:8.5f} | {Km:9.5f} | {Fm:9.5f} |\n"
        if F(Xi) * F(Xs) < 0:
            Xs = Km
        else:
            Xi = Km
    #Salida del método
    return Km, No, Tb

# Ejecución del método
x, n, tabla = iterativo(Kmin, Kmax, 20)

# Salida
print(f"---------------------------------")
print(f"| Número de Iteraciones:    {n}  |")
print(f"---------------------------------")
print(f"| Tiempo aproximada: {x:10.5f} |")
print(f"| altura aproximada: {F(x):10.5f} |")
print(f"---------------------------------")
```

    ---------------------------------
    | Número de Iteraciones:    20  |
    ---------------------------------
    | Tiempo aproximada:    2.01562 |
    | altura aproximada:    0.15935 |
    ---------------------------------

```python
print(tabla)
```

    | I # |       Xi |       Xs |        Xm |        Fm |
    -----------------------------------------------------
    |   1 |  2.00000 |  3.00000 |   2.50000 |  -6.46773 |
    |   2 |  2.00000 |  2.50000 |   2.25000 |  -2.75968 |
    |   3 |  2.00000 |  2.25000 |   2.12500 |  -1.13575 |
    |   4 |  2.00000 |  2.12500 |   2.06250 |  -0.38132 |
    |   5 |  2.00000 |  2.06250 |   2.03125 |  -0.01848 |
    |   6 |  2.00000 |  2.03125 |   2.01562 |   0.15934 |
    |   7 |  2.00000 |  2.01562 |   2.00781 |   0.24735 |
    |   8 |  2.00781 |  2.01562 |   2.01172 |   0.20342 |
    |   9 |  2.01172 |  2.01562 |   2.01367 |   0.18140 |
    |  10 |  2.01367 |  2.01562 |   2.01465 |   0.17037 |
    |  11 |  2.01465 |  2.01562 |   2.01514 |   0.16486 |
    |  12 |  2.01514 |  2.01562 |   2.01538 |   0.16210 |
    |  13 |  2.01538 |  2.01562 |   2.01550 |   0.16072 |
    |  14 |  2.01550 |  2.01562 |   2.01556 |   0.16003 |
    |  15 |  2.01556 |  2.01562 |   2.01559 |   0.15968 |
    |  16 |  2.01559 |  2.01562 |   2.01561 |   0.15951 |
    |  17 |  2.01561 |  2.01562 |   2.01562 |   0.15942 |
    |  18 |  2.01562 |  2.01562 |   2.01562 |   0.15938 |
    |  19 |  2.01562 |  2.01562 |   2.01562 |   0.15936 |
    |  20 |  2.01562 |  2.01562 |   2.01562 |   0.15935 |

## 3. Convergencia
---
### a. Convergencia de un método iterativo
Se entiende por convergencia de un método numérico la propiedad que cumple este método de aproximarse a un valor limite $\color{#a78a4d}{L}$, al realizar un gran número de iteraciones. En la medida en la que un método numérico requiera de un menor número de iteraciones que otro, para acercarse al limite, se dice que tiene una mayor rapidez de convergencia y si este método no converge a un limite se dice que es un método divergente.

\begin{align}
L &= \lim_{n \ \rightarrow \ + \infty} x_n\\
\end{align}

>**NOTA:** Dado que estos métodos forman una base, el método converge en N iteraciones, donde N es el tamaño del sistema; sin embargo, esta afirmación no se sostiene debido a errores de redondeo en las aproximaciones sucesivas y el análisis de la convergencia se complica en función de las ecuaciones que consiguen la solución en cada iteración.

### b. Análisis de la convergencia
En la práctica N puede ser muy grande y si el proceso iterativo converge, podría alcanzar una precisión suficiente mucho antes de dicha convergencia; por esto es necesario valorar la rapidez con que un método particular converge a una solución con la precisión requerida.

* _**Error de iteración:**_ El error real y absoluto en la n-esima iteración es calculado como la distancia entre la solución aproximada de esa iteración $\color{#a78a4d}{x_n}$ y el limite de convergencia $\color{#a78a4d}{L}$ del método iterativo, que se considera la solución "Real".

\begin{align}
E(x_n) & = x_n \text{ - } L \\
e_a(x_n) & = | x_n \text{ - } L | \\
\end{align}

* _**Orden de convergencia:**_ El orden de convergencia de un método iterativo es necesario para comparar la efectividad de los diferentes métodos iterativos; de forma que se pueda caracterizar la disminución del error en cada iteración. En métodos numéricos el orden de convergencia $\color{#a78a4d}{q}$ es una medida de la rapidez con la que el método se acerca a su valor limite $\color{#a78a4d}{L}$ con una tasa de convergencia $\color{#a78a4d}{\mu}$; en particular, si $\color{#a78a4d}{q=1}$, la convergencia es lineal; si $\color{#a78a4d}{q=2}$, la convergencia es cuadrática y si $\color{#a78a4d}{q=3}$, el orden es 3 y es la convergencia es cúbica.

\begin{align}
\mu &= \lim_{k \rightarrow + \infty} \frac{ e_a(x_{n+1}) }{ e_a(x_n)^q}, \quad (\mu > 0)\\
\mu &= \lim_{k \rightarrow + \infty} \frac{| x_{n+1} \text{ - } L |}{| x_n \text{ - } L |^q} \\
q &\approx \frac{ \log_{10} \left| \frac{x_{n+1} \text{ - } x_n}{x_n \text{ - } x_{n-1}} \right|}{\log_{10} \left| \frac{x_n \text{ - } x_{n-1}}{x_{n-1} \text{ - } x_{n-2}} \right|} \\
\end{align}

* _**Criterios de aproximación:**_ Un criterio de aproximación para un método iterativo es una condición que permite detener la ejecución del algoritmo cuando se alcanza una solución con la precisión deseada; es decir, cuando el nuevo valor calculado está lo suficientemente cerca de los anteriores. Este condición permite detener el algoritmo comparando la solución alcanzada en la iteración $\color{#a78a4d}{n}$ con el valor de tolerancia $\color{#a78a4d}{\epsilon}$, aceptado; donde la tolerancia es un número positivo, lo suficientemente pequeño.

\begin{align}
e_r & = \frac{| x_n - x_{n+1} |}{| x_n |} < \epsilon \\
\end{align}

```python
# Defino el método iterativo de la Bisección
def _biseccion(Xmin, Xmax, Imax, Tmax):
    """ Método de la Bisección para encontrar raíces en un intervalo.

        ## Parámetros:
            Xmin (int): limite inferior de intervalo.
            Xmax (int): limite superior de intervalo.
            Imax (int): número máximo de iteraciones.
            Tmax (int): exponente de tolerancia máxima, T = 1e^Tmax.

        ## Devoluciones:
            Km (float): valor de x encontrado.
            No (int)  : iteraciones.
            Tb (str): tabla con evoluvión de iteraciones.
    """
    # Inicializar variables
    Xs = Xmax
    Xi = Xmin
    Ea = []
    Tb = f"| I # | {'Xi':>8} | {'Xs':>8} | {'ΔX':>9} | {'Xm':>9} | {'Error Abs':>9} | {'Fm':>9} |\n"
    Tb += "-----------------------------------------------------------------------------\n"
    # Iteraciones
    for No in range(1, Imax + 1):
        ΔX = abs(Xs - Xi)
        ea = ΔX / 2
        Ea.append(ea)
        Xm = (Xs + Xi) / 2
        Fm = F(Xm)
        Tb += f"| {No:3} | {Xi:8.5f} | {Xs:8.5f} | {ΔX:9.5f} | {Xm:9.5f} | {ea:9.5f} | {Fm:9.5f} |\n"
        if ea < (10**Tmax): break
        if F(Xi) * F(Xm) < 0:
            Xs = Xm
        else:
            Xi = Xm
    #Salida del método
    return Xm, No, ea, Tb

# Ejecución del método
Orden_Tolerancia = -4
Max_iteraciones = 50
x, n, ea, tabla = _biseccion(Kmin, Kmax, Max_iteraciones, Orden_Tolerancia)

# Salida
print(f"-------------------------------------")
print(f"| Número de Iteraciones:         {n} |")
print(f"-------------------------------------")
print(f"| Tolerancia:                  1e{Orden_Tolerancia} |")
print(f"| Error absoluto:         {ea:.3e} |")
print(f"-------------------------------------")
print(f"| Tiempo aproximada: {x:14.9f} |")
print(f"| altura aproximada: {F(x):14.9f} |")
print(f"-------------------------------------")
```

    -------------------------------------
    | Número de Iteraciones:         14 |
    -------------------------------------
    | Tolerancia:                  1e-4 |
    | Error absoluto:         6.104e-05 |
    -------------------------------------
    | Tiempo aproximada:    2.029602051 |
    | altura aproximada:    0.000382908 |
    -------------------------------------

```python
print(tabla)
```

    | I # |       Xi |       Xs |        ΔX |        Xm | Error Abs |        Fm |
    -----------------------------------------------------------------------------
    |   1 |  2.00000 |  3.00000 |   1.00000 |   2.50000 |   0.50000 |  -6.46773 |
    |   2 |  2.00000 |  2.50000 |   0.50000 |   2.25000 |   0.25000 |  -2.75968 |
    |   3 |  2.00000 |  2.25000 |   0.25000 |   2.12500 |   0.12500 |  -1.13575 |
    |   4 |  2.00000 |  2.12500 |   0.12500 |   2.06250 |   0.06250 |  -0.38132 |
    |   5 |  2.00000 |  2.06250 |   0.06250 |   2.03125 |   0.03125 |  -0.01848 |
    |   6 |  2.00000 |  2.03125 |   0.03125 |   2.01562 |   0.01562 |   0.15934 |
    |   7 |  2.01562 |  2.03125 |   0.01562 |   2.02344 |   0.00781 |   0.07073 |
    |   8 |  2.02344 |  2.03125 |   0.00781 |   2.02734 |   0.00391 |   0.02620 |
    |   9 |  2.02734 |  2.03125 |   0.00391 |   2.02930 |   0.00195 |   0.00387 |
    |  10 |  2.02930 |  2.03125 |   0.00195 |   2.03027 |   0.00098 |  -0.00730 |
    |  11 |  2.02930 |  2.03027 |   0.00098 |   2.02979 |   0.00049 |  -0.00171 |
    |  12 |  2.02930 |  2.02979 |   0.00049 |   2.02954 |   0.00024 |   0.00108 |
    |  13 |  2.02954 |  2.02979 |   0.00024 |   2.02966 |   0.00012 |  -0.00032 |
    |  14 |  2.02954 |  2.02966 |   0.00012 |   2.02960 |   0.00006 |   0.00038 |

## 4. Estabilidad
---
Dado que algunos métodos numéricos no siempre convergen, se entiende por estabilidad de un método numérico el nivel de garantía de dicha convergencia; en la medida en la que un método numérico, ante una muy amplia gama de posibilidades de modelado matemático, es más seguro que converja que otro, entonces se dice que tiene una mayor estabilidad.

* _**Criterio de la perturbación inicial:**_ Si definimos esta perturbación como el error introducido al finalizar la primera iteración, este criterio propone que la perturbación inicial debe estar acotada.

* _**criterio de la propagación del error:**_  Si definimos la propagación como el efecto de la perturbación en la solución debida a la perturbación inicial, este criterio propone una pequeña perturbación inicial no debe implicar más que una pequeña perturbación en la solución, independientemente del paso.

* _**Criterio de linealidad:**_ la propagación lineal de los errores en cada iteración suele ser inevitable, pero permite resultados aceptables en la mayoría de los métodos; en cambio el error crece rápidamente en cada iteración con una propagación exponencial de los errores, lo que finalmente conduce a soluciones inexactas y métodos inestables.

### Ejemplo: Propagación del error (Convergencia vs Estabilidad)
---
Para tener una idea de por qué importa más la potencia en $\color{#a78a4d}{\Delta x}$ al considerar la convergencia, la siguiente figura muestra cómo las diferentes potencias en la tasa de convergencia pueden afectar la rapidez con la que converge nuestra solución.

Representar el error como una función de $\color{#a78a4d}{\Delta x}$ en una gráfica es una forma común de mostrar que un método numérico está haciendo lo que esperamos y muestra el comportamiento de convergencia correcto y dado que los errores pueden reducirse rápidamente, es muy común trazar este tipo de gráficos en una escala log-log para visualizar fácilmente los resultados.

>**NOTA:** Tenga en cuenta que aquí estamos dibujando los mismos datos de dos maneras diferentes y que si un método fuera realmente del orden $n$, será una función lineal en el espacio log-log con pendiente n.

```python
# Vector de variable independiente
dx = np.linspace(1.0, 1e-4, 100)

# Gráfica en el plano de el cambio de la variable dependiente con el cambio de la variable independiente
fig = plt.figure()
fig.set_figwidth(fig.get_figwidth() * 2.0)
fig.suptitle("Crecimiento de las diferencias".upper(), fontsize=16, fontweight="bold")

axes = []
axes.append(fig.add_subplot(1, 2, 1))
axes.append(fig.add_subplot(1, 2, 2))

# Vectores de variable dependiente dy = dx^n
for n in range(1, 5): # [1, 2, 3, 4]
    axes[0].plot(dx, dx**n, label="$\Delta x^%s$" % n)
    axes[1].loglog(dx, dx**n, label="$\Delta x^%s$" % n)

# Escalando la gráfica
axes[1].set_xticks([10.0**(-n) for n in range(5)])
axes[1].set_yticks([10.0**(-n) for n in range(16)])

# Textos que aparecen en la gráfica
axes[0].set_title("Eje líneal", fontsize=8)
axes[0].set_xlabel("$\Delta x$", fontweight="bold")
axes[0].set_ylabel("Error estimado", fontweight="bold")
axes[0].legend(loc=2)

axes[1].set_title("Eje Log-Log", fontsize=8)
axes[1].set_xlabel("$\Delta x$", fontweight="bold")
axes[1].set_ylabel("Error estimado", fontweight="bold")
axes[1].legend(loc=4)

plt.show()
```

---
## Mas Recursos

- [Propagación de errores](https://en.wikipedia.org/wiki/Rounding) (Wikipedia)
- [Método iterativo](https://es.wikipedia.org/wiki/M%C3%A9todo_iterativo) (Wikipedia)
- [Estabilidad numérica](https://es.wikipedia.org/wiki/Estabilidad_num%C3%A9rica) (Wikipedia)
- [Orden de convergencia](https://es.wikipedia.org/wiki/Orden_de_convergencia) (Wikipedia)