# Shallow water models of coastal upwelling

This notebook summarizes several attempts to model coastal upwelling in the Strait of Georgia using the shallow water equations. These modelling attempts are an effort to explain the dynamics of enhanced upwelling around capes that we observe in the SalishSeaCast model (see [**Motivation**](#Motivation)). The following cases are explored:

   1. [**Linear time dependence**](#Linear-time-dependence)
       * [Surface height sink near coast](#Surface-height-sink-near-coast)
       * [Linear bottom friction](#Linear-bottom-friction)
       * [Longshore windstress](#Longshore-windstress)
   2. [**Steady state**](#Steady-state)
       * [Linear bottom friction, non-zero depth gradient](#Linear-bottom-friction,-non-zero-depth-gradient)
       * [Longshore bottom friction, cross-shelf bottom slope](#Longshore-bottom-friction,-cross-shelf-bottom-slope)

***

### Preamble

```python
import numpy as np
import xarray as xr
import matplotlib.pyplot as plt
import gsw
import os
import warnings
import copy
from cmocean import cm
from salishsea_tools import viz_tools, grid_tools

# Global constants
f = 1e-4      # [s-1]
g = 9.81      # [m s-2]

%matplotlib inline
plt.rcParams['font.size'] = 14
warnings.simplefilter('ignore')
```

***

### Local functions

```python
def load_results(t, rundir, prefix, maskfile, xrange=[None], yrange=[None]):
    """Load SalishSeaCast velocities, tracers, and grid parameters
    """

    # Load netCDF files
    T = xr.open_dataset(os.path.join(rundir, f'{prefix}_grid_T.nc'))
    U = xr.open_dataset(os.path.join(rundir, f'{prefix}_grid_U.nc'))
    V = xr.open_dataset(os.path.join(rundir, f'{prefix}_grid_V.nc'))
    mask = xr.open_dataset(maskfile)

    # Define slices
    xslice = slice(*xrange)
    yslice = slice(*yrange)

    # Define parameters
    params = {
        'x': mask.x[xslice].values,
        'y': mask.y[yslice].values,
        'u': U.vozocrtx[t, :, yslice, xslice].values,
        'v': V.vomecrty[t, :, yslice, xslice].values,
        'eta': T.sossheig[t, yslice, xslice].values,
        'tmask': mask.tmask[0, :, yslice, xslice].values,
        'gdept_0': mask.gdept_0[0, :, yslice, xslice].values,
        'e1t': mask.e1t[0, yslice, xslice].values,
        'e2t': mask.e2t[0, yslice, xslice].values,
        'e3t_0': mask.e3t_0[0, :, yslice, xslice].values,
    }

    # Obtain the time dependent grid parameters
    VVL = grid_tools.calculate_time_dependent_grid(
        params['e3t_0'], params['tmask'], params['eta'][np.newaxis, ...],
        {'e3t_t': params['e3t_0'][np.newaxis, ...], 'gdept_t': params['gdept_0'][np.newaxis, ...]},
    )

    # Calculate rho
    rho = gsw.rho(T.vosaline[t, :, yslice, xslice], T.votemper[t, :, yslice, xslice], VVL['gdept_t'][0, ...])

    # Finalize output dict
    params.update({
        'rho': rho,
        'e3t_t': VVL['e3t_t'][0, ...],
        'gdept_t': VVL['gdept_t'][0, ...],
    })

    return params
```

```python
def calc_geostrophic_velocities(z, params, corr=False):
    """Calculate pressure gradients and geostrophic velocities at idepth
    """

    # Geostrophic parameters dict
    GEO = {}

    # Calculate z surface displacement xi
    stretching = params['e3t_t'][:z, ...].sum(axis=0) - params['e3t_0'][:z, ...].sum(axis=0)
    xi = params['eta'] - stretching

    # Define rho at z surface
    rho_bot = params['rho'][z, ...]
    rho_bot[xi < 0] = params['rho'][z - 1, ...][xi < 0]

    # Calculate pressure
    GEO['pressure'] = g * ((params['rho'][:z, ...] * params['e3t_t'][:z, ...]).sum(axis=0) + rho_bot * xi)

    # Extract and unstagger the model velocity fields
    GEO['u'] = params['u'][z, ...]
    GEO['v'] = params['v'][z, ...]
    GEO['u'][1:, 1:], GEO['v'][1:, 1:] = viz_tools.unstagger(GEO['u'], GEO['v'])

    # Calculate the pressure gradient
    dpdy, dpdx = np.gradient(GEO['pressure'], axis=(0, 1))

    # Calculate the geostrophic velocities
    GEO['u_g'] = -1 / (f * params['rho'][z, ...]) * dpdy / params['e2t']
    GEO['v_g'] = 1 / (f * params['rho'][z, ...]) * dpdx / params['e1t']

    # Calculate ageostrophic velocities
    GEO['u_a'] = GEO['u'] - GEO['u_g']
    GEO['v_a'] = GEO['v'] - GEO['v_g']

    return GEO
```

```python
def plot_results(params, GEO):
    """Plot SalishSeaCast results
    """

    # Make figure panels
    fig, axs = plt.subplots(1, 4, figsize=(17, 8))
    caxs = [fig.add_axes([xbar, 0.05, 0.17, 0.015]) for xbar in [0.125, 0.325]]

    # Plot results
    cs, qs = [0, 0], [0, 0]
    uindex = (abs(GEO['u_g']) > 0.1) | (abs(GEO['v_g']) > 0.1)
    u_g = np.ma.masked_where(uindex, GEO['u_g'])
    v_g = np.ma.masked_where(uindex, GEO['v_g'])
    cs[0] = axs[0].contourf(params['x'], params['y'], params['eta'], levels=np.arange(0, 0.12, 0.005), cmap=cm.deep, extend='both')
    cs[1] = axs[1].contourf(params['x'], params['y'], GEO['pressure']*1e-4, levels=np.arange(39.083, 39.13, 0.001), cmap=cm.dense, extend='both')
    qs[0] = axs[2].quiver(params['x'][::5], params['y'][::5], GEO['u'][::5, ::5], GEO['v'][::5, ::5], width=0.004, scale=1.8)
    qs[1] = axs[3].quiver(params['x'][::5], params['y'][::5], u_g[::5, ::5], v_g[::5, ::5], width=0.004, scale=1.8)

    # Format panels
    for ax, title in zip(axs, ['$\eta$', '$P_{40m}$', 'u$_{40m}$', 'u$_{40m}$ (geostrophic)']):
        ax.contourf(params['x'], params['y'], params['tmask'][0, ...], levels=[-0.01, 0.01], colors='burlywood')
        ax.contour(params['x'], params['y'], params['tmask'][0, ...], levels=[-0.01, 0.01], colors='k')
        ax.set_title(title)
        ax.set_xlabel('Grid X')
        if title is '$\eta$':
            ax.set_ylabel('Grid Y')
        else:
            ax.yaxis.set_ticklabels([])
        viz_tools.set_aspect(ax)

    # Add colorbars
    for cax, c, q, ticks, label in zip(caxs, cs, qs, [[0, 0.05, 0.1], [39.09, 39.11, 39.13]], ['[m]', '[dbar]']):
        plt.colorbar(c, cax=cax, orientation='horizontal', ticks=ticks, label=label)
        qkey = ax.quiverkey(q, 0.8, 0.75, 0.1, '0.1 m/s')

    plt.show()

    return
```

***

## Motivation

We will examine the SalishSeaCast results for 28 April, 2015 using the lake domain with closed boundaries (no tides).

```python
# Load and plot SalishSeaCast results for Baynes Sound upwelling event
rundir = '/data/vdo/MEOPAR/completed-runs/candidates/baynessound/apr15lake'
prefix = 'SalishSea_1h_20150424_20150429'
maskfile = '/data/bmoorema/MEOPAR/grid/mesh_mask201702.nc'
z = 23
t = 70
lims = [110, 250, 500, 750]
params = load_results(t, rundir, prefix, maskfile, xrange=lims[:2], yrange=lims[2:])
GEO = calc_geostrophic_velocities(z, params)
plot_results(params, GEO)
```

**We observe the following during this event:**
   1. Surface height moves laterally offshore ($\eta$ panel)
   2. Pressure at 40 m sets up in the longshore direction ($P_{40\text{m}}$ panel)
   3. Total velocity field at 40 m flows dominantly to the south ($\mathbf{u}_{40\text{m}}$ panel)
   4. Geostrophic velocity field at 40 m is onshore, consistent with the 40 m pressure field ($\mathbf{u}_{40\text{m}}$ (geostrophic) panel)
   5. Enhanced upwelling results on the seaward edge of Denman and Hornby Islands (not shown)

**We seek a simplified, conceptual model that**
   1. Reproduces the first-order structure of the SalishSeaCast results
   2. Can be used to explain the mechanics of the enhanced upwelling near Denman and Hornby Islands

**Plan**

We will experiment with various terms in the barotropic shallow water equations. If we find a promising model, we will consider baroclinity either by using a 1.5 or 2 layer model or by considering mass conservation.

***

## Shallow water model

The family of models in this notebook are based on the linearized, time-dependent shallow water equations

\begin{align}
\frac{\partial\mathbf{u}}{\partial t} + f\mathbf{k}\times\mathbf{u} & = -g\nabla\eta \tag{1a} \\
\frac{\partial\eta}{\partial t} + \nabla\cdot h\mathbf{u} & = 0 \tag{1b}
\end{align}

***

## Linear time dependence

In order to avoid a strictly steady-state model, we seek solutions ($\nabla h = 0$) to (1) that are both linear and constant in time, e.g.,

\begin{align}
\mathbf{u} & = \mathbf{u}_1ft + \mathbf{u}_0 \tag{2a} \\
\eta & = \eta_1ft + \eta_0 \tag{2b}
\end{align}

The equations proportional to $t$ are thus

\begin{align}
f\mathbf{k}\times\mathbf{u}_1 & = -g\nabla\eta_1 \tag{3a} \\
\nabla\cdot\mathbf{u}_1 & = 0 \tag{3b}
\end{align}

and the steady equations are thus

\begin{align}
f\mathbf{u}_1 + f\mathbf{k}\times\mathbf{u}_0 & = -g\nabla\eta_0 \tag{4a} \\
f\eta_1 + h\nabla\cdot\mathbf{u}_0 & = 0 \tag{4b}
\end{align}

Since the growing velocity $\mathbf{u}_1$ is in geostrophic balance (3a), ageostrophic velocities associated with Ekman transport must be part of the steady velocity $\mathbf{u}_0$. If we let $\mathbf{u}_0 = \mathbf{u}_g + \mathbf{u}_a$ such that

$$f\mathbf{k}\times\mathbf{u}_g = -g\nabla\eta_0 \tag{5}$$

then the steady ageostrophic velocity $\mathbf{u}_a$ (Ekman transport) feeds the growing geostrophic velocity $\mathbf{u}_1$

$$\mathbf{u}_1 + \mathbf{k}\times\mathbf{u}_a = 0 \tag{6}$$

or the growing surface height gradient (substituting 3a)

$$\mathbf{u}_a = -\frac{g}{f}\nabla\eta_1 \tag{7}$$

In order to use this model, we need to force the surface height field $\eta$. Options are as follows.

***

### Surface height sink near coast

(Susan's canyon model, Allen 1996)

Adding a coastal surface sink $q$ to (1b) such that

$$\frac{\partial\eta}{\partial t} + h\nabla\cdot\mathbf{u} = -q \tag{8}$$

results in the following vorticity balance (curl of 1a but considering 8)

$$\frac{\partial}{\partial t}\left(h\nabla\times\mathbf{u} - f\eta\right) = fq \tag{9}$$

Substituting (2) into (9) along with the momentum and continuity balances gives the following equations for the time-dependent ($\eta_1$) and constant ($\eta_0$) surface heights

\begin{align}
R^2\nabla^2\eta_1 - \eta_1 & = \frac{q}{f} \tag{10a} \\
R^2\nabla^2\eta_0 - \eta_0 & = 0 \tag{10b} \\
\end{align}

where $R = \sqrt{gh}/f$ is the barotropic Rossby radius.

Equation (10a) is solved as the sum of homogeneous and particular solutions

$$\eta_1(x, y) = \eta_{1_H}(x, y) + \eta_{1_P}(x) \tag{11}$$

Since $q$ is confined to a region $x < E$, $\eta_P$ (given in Allen 1996) is defined piecewise

$$\eta_{1_P}(x) =
\begin{cases}
\frac{q_0}{f}\left[\left(\frac{\alpha\sinh\left(\frac{S - E}{R}\right) + \cosh\left(\frac{S - E}{R}\right)}{\alpha\sinh\left(\frac{S}{R}\right) + \cosh\left(\frac{S}{R}\right)}\right)\cosh\left(\frac{x}{R}\right) - 1\right],  & \quad x < E \\
-\frac{q_0}{f}\left(\frac{\sinh\left(\frac{E}{R}\right)}{\alpha\sinh\left(\frac{S}{R}\right) + \cosh\left(\frac{S}{R}\right)}\right)\left[\alpha\cosh\left(\frac{S - x}{R}\right) + \sinh\left(\frac{S - x}{R}\right)\right],  & \quad E < x < S
\end{cases} \tag{12}
$$

where $\alpha$ is the depth ratio over across the shelfbreak. $\eta_{1_H}$ is solved in the absence of $q$ such that $\eta_1=0$ at the coast and shelfbreak and decays as $y \to -\infty$. The solution is given in Allen (1996).

$$\eta_{1_H} = \sum_{n=1}^\infty c_n\exp\left\{-\big|\hspace{0.1cm}y\hspace{0.1cm}\big|\left[\frac{1}{R^2} + \left(\frac{n\pi}{S}\right)^2\right]^{1/2}\right\}\sin\left(\frac{n\pi x}{S}\right) \tag{13}$$

The $c_n$ coefficients determine the streamlines against and around the cape (at $y=0$).

\begin{align}
\eta_{1_H}(x, 0) & = \eta_{1_P}(0) - \eta_{1_P}(x) & \text{for } x = [0, M] \tag{14a}\\
\frac{\partial}{\partial x}\eta_{1_H}(x, 0) & = 0 & \text{for } x = [M, S] \tag{14b}
\end{align}

```python
# Parameters
tau = 1e-4    # Kinematic windstress         [m2 s-2]
H_s = 50      # Depth over shelf             [m]
H_d = 5e2     # Depth beyond shelfbreak      [m]
S = 1e4       # Shelf width                  [m]
E = 1e3       # Upwelling lengthscale        [m]
M = S/2       # Cape length                  [m]
N = 1000      # Fourier series limit

# Terms
R = np.sqrt(g * H_s) / f     # Rossby radius [m]
alpha = np.sqrt(H_s / H_d)   # Depth ratio
q_0 = tau / (E * f)  # Ekman velocity [m/s]
```

```python
def calc_eta_P(x, q_0, f, R, S, E):
    """Calculate the particular solution eta_P(x)
    """

    top = alpha * np.sinh((S - E) / R) + np.cosh((S - E) / R)
    bot = alpha * np.sinh(S / R) + np.cosh(S / R)
    eta_P = -q_0 / f * np.sinh(E / R) / bot * (alpha * np.cosh((S - x) / R) + np.sinh((S - x) / R))
    eta_P[x <= E] = q_0 / f * (top / bot * np.cosh(x[x <= E] / R) - 1)

    return eta_P
```

```python
# Calculate C coefficients according to y=0 boundary conditions

# Coordinates
x_ni = np.arange(1, N + 1) / N * S
ni = np.arange(1, N + 1, dtype=int)
n, x_n = np.meshgrid(ni, x_ni)
xindex = x_ni <= M

# Allocate matrix A and RHS
A = np.zeros([N, N])
RHS = np.zeros([N, 1])

# Populate A matrix
A[xindex, :] = np.sin(n[xindex, :] * np.pi * x_n[xindex, :] / S)
A[~xindex, :] = np.sqrt(1 / R**2 + (n[~xindex, :] * np.pi / S)**2) * np.sin(n[~xindex, :] * np.pi * x_n[~xindex, :] / S)

# Populate RHS
RHS[xindex, 0] = calc_eta_P(np.array([0]), q_0, f, R, S, E) - calc_eta_P(x_ni[xindex], q_0, f, R, S, E)

# Solve for C coefficients
C = np.linalg.solve(A, RHS)
```

```python
# Calculate eta_H
xi = np.arange(0, S, S*1e-2)
yi = np.arange(-S, S, S*1e-2)
x, y = np.meshgrid(xi, yi)
eta_H = np.zeros(x.shape)
for n, C_n in zip(range(1, N + 1), C):
    eta_H = eta_H + C_n * np.exp(-abs(y) * np.sqrt(1 / R**2 + (n * np.pi / S)**2)) * np.sin(n * np.pi * x / S)

# Calculate eta
eta_P = calc_eta_P(x, q_0, f, R, S, E)
eta = eta_H + eta_P

# Calculate u_a
eta_grad = np.gradient(eta)
u_a = g / f * eta_grad[1] / S*1e-2
v_a = g / f * eta_grad[0] / S*1e-2
```

```python
# Plot
fig, ax = plt.subplots(1, 1, figsize=(8, 16))
ax.contour(xi/S, yi/S, eta, colors='k')
ax.quiver(xi[::5]/S, yi[::5]/S, u_a[::5, ::5], v_a[::5, ::5], width=0.003, scale=1e-4)
ax.plot([0, M/S], [0, 0], 'k-')
for tick in np.arange(-1, 1, 0.02):
    ax.plot([-0.01, 0], [tick, tick+0.01], 'k-', clip_on=False)
for tick in np.arange(0, M/S, 0.02):
    ax.plot([tick, tick+0.01], [-0.01, 0], 'k-', clip_on=False)
ax.set_xlabel('$x/S$')
ax.set_ylabel('$y/S$')
ax.xaxis.set_ticks([0, 1])
ax.yaxis.set_ticks([-1, 0, 1])
ax.tick_params(direction='out', pad=8)
ax.set_xlim([0, 1])
ax.set_ylim([-1, 1])
ax.text(0.02, 0.96, 'Low $\eta$', transform=ax.transAxes)
ax.text(0.85, 0.96, 'High $\eta$', transform=ax.transAxes)
ax.text(0.03, 0.7, '$\\tau_y$', transform=ax.transAxes)
ax.arrow(0.04, 0.72, 0, 0.1, transform=ax.transAxes, head_width=0.01, facecolor='k')
ax.set_title('Linear time dependence solution (with $q$)')

plt.show()
```

The geostrophic streamlines distort around the cape and the ageostrophic velocities focus and intensify near the cape.

**Problem**

Since the coastal boundaries in this model are in geostrophic balance (according to 3a and 4a), the streamlines conform to all coastal features. However, numerical results show a longshore pressure setup that drives geostrophic flow onshore.

A better model would have a mechanism to supply the coastal upwelling with geostrophic onshore flow rather than just ageostrophic flow. Thus, we seek a model that balances the longshore pressure gradient at the coast.

***

### Linear bottom friction

Bottom friction is a logical forcing to balance the surface height at the coast. We can add linear friction to (1a) such that

$$\frac{\partial\mathbf{u}}{\partial t} + f\mathbf{k}\times\mathbf{u} = -g\nabla\eta - \frac{\mu}{h}\mathbf{u} \tag{15}$$

The potential vorticity balance is then

$$\frac{\partial}{\partial t}\left(h\nabla\times\mathbf{u} - f\eta\right) = -\mu\nabla\times\mathbf{u} + fq \tag{16}$$

**Problem**

The part of (16) proportional to $t$ is

$$\nabla\times\mathbf{u}_1 = 0 \tag{17}$$

which implies, considering the part of (15) proportional to $t$, that

$$\nabla^2\eta_1 = 0 \tag{18}$$

So linear bottom friction with linear time dependence gives only trivial solutions for the growing $\mathbf{u}_1$ and $\eta_1$ fields. We need another way to balance the surface height setup at the coast.

***

### Longshore windstress

Longshore windstress at the coast provides another way to balance the longshore surface height gradient. We can add longshore windstress $\tau_y$ to (1a) such that

$$\frac{\partial\mathbf{u}}{\partial t} + f\mathbf{k}\times\mathbf{u} = -g\nabla\eta + \tau_y{\hat{\jmath}} \tag{19}$$

If the windstress curl is zero, potential vorticity is conserved.

$$\frac{\partial}{\partial t}\left(h\nabla\times\mathbf{u} - f\eta\right) = 0 \tag{20}$$

Substituting (2) into (19) and (20) gives similar equations to (10) except the growing solution is now unforced (homogeneous).

$$R^2\nabla^2\eta_1 - \eta_1 = 0 \tag{21} $$

Windstress forcing in this case comes from the coastal boundary condition

\begin{align}
\frac{\partial\eta_1}{\partial y} & = 0 \tag{22a} \\
\frac{\partial\eta_1}{\partial x} & = -\frac{\partial\eta_0}{\partial y} + \frac{\tau_y}{g} \tag{22b}
\end{align}

The growing surface height field is geostrophic at the coast, and is not fully constrained so will yield a trivial solution. The steady field however is balanced by the longshore windstress $\tau_y$. Equation (7) thus becomes

$$\mathbf{u}_a = \hat{\mathbf{k}}\times\tau_y \tag{23}$$

Thus the ageostrophic velocity is a constant cross-shore velocity perpendicular to the windstress. This solution could still be interesting though since part of the cross-shore flow is geostrophic.

***

## Steady state

Neglecting time dependence entirely, the linearized, steady state shallow water equations are

\begin{align}
f\mathbf{k}\times\mathbf{u} & = -g\nabla\eta \tag{24a} \\
0 & = \nabla\cdot h\mathbf{u} \tag{24b}
\end{align}

The momentum balance (24a) is entirely geostrophic so the curl of (24a) results in only trivial solutions

$$\nabla\cdot\mathbf{u} = 0 \tag{25}$$

We need additional forces in order to balance the divergence.

***

### Linear bottom friction, non-zero depth gradient

Take the linearized, steady state shallow water equations with linear bottom friction.

\begin{align}
f\mathbf{k}\times\mathbf{u} & = -g\nabla\eta - \frac{\mu}{h}\mathbf{u} \tag{26a} \\
0 & = \nabla\cdot h\mathbf{u} \tag{26b}
\end{align}

Obtain the vorticity balance by taking the curl of (26a) and substituting (26b)

$$\frac{1}{g}\left(f^2 + \frac{\mu^2}{h^2}\right)\mathbf{u}\cdot\nabla h = \mu\nabla^2\eta \tag{27}$$

Non-trivial solutions exist only for a non-zero bottom gradient. Therefore we let $\nabla h \neq 0$. Solve (26a) for $u$ and $v$

\begin{align}
\left(f^2 + \frac{\mu^2}{h^2}\right)\frac{u}{g} & = -f\frac{\partial\eta}{\partial y} - \frac{\mu}{h}\frac{\partial\eta}{\partial x} \tag{28a}\\
\left(f^2 + \frac{\mu^2}{h^2}\right)\frac{v}{g} & = f\frac{\partial\eta}{\partial x} - \frac{\mu}{h}\frac{\partial\eta}{\partial y} \tag{28b}
\end{align}

and substitute (28) into (27) to isolate $\eta$

$$ -\left(f\frac{\partial\eta}{\partial y} + \frac{\mu}{h}\frac{\partial\eta}{\partial x}\right)\frac{\partial h}{\partial x} + \left(f\frac{\partial\eta}{\partial x} - \frac{\mu}{h}\frac{\partial\eta}{\partial y}\right)\frac{\partial h}{\partial y} = \mu\nabla^2\eta \tag{29}$$

For simplicity, we can only consider a linear bottom slope in the cross-shore direction. Let $\frac{\partial h}{\partial y} = 0$, $\frac{\partial h}{\partial x} = s$

$$ \frac{1}{s}\nabla^2\eta + \frac{1}{h}\frac{\partial\eta}{\partial x} + \frac{f}{\mu}\frac{\partial\eta}{\partial y} = 0 \tag{30}$$

Equation (30) is a linear PDE and potentially solveable. But we can make further simplifications to (26).

***

### Longshore bottom friction, cross-shelf bottom slope

Take the linearized, steady state shallow water equations with linear friction and longshore windstress. Assume cross-shore geostrophic balance.

\begin{align}
f\mathbf{k}\times\mathbf{u} & = -g\nabla\eta + \frac{1}{h}\left(\tau_y - \mu v\right)\hat{\jmath} \tag{31a} \\
0 & = \nabla\cdot h\mathbf{u} \tag{31b}
\end{align}

The geostrophic velocity balances the surface height gradient while the ageostrophic velocity $u_a$ (cross-shelf only) balances windstress and friction through Coriolis and geostrophy. The ageostrophic transport is

$$u_ah = \frac{1}{f}\left(\tau_y - \frac{\mu g}{f}\frac{\partial\eta}{\partial x}\right) \tag{32}$$

Taking the curl of (31a) and considering (31b), the vorticity balance yields

$$\frac{f^2}{\mu g}u = \frac{1}{h_x}\frac{\partial^2\eta}{\partial x^2} - \frac{1}{h}\frac{\partial\eta}{\partial x} \tag{33}$$

Substituting (31a) gives the heat equation analog

$$\frac{\partial^2\eta}{\partial x^2} - \frac{1}{\kappa}\frac{\partial\eta}{\partial y} = 0, \hspace{0.5cm} \frac{1}{\kappa} = -\frac{f}{\mu}\frac{\partial h}{\partial x}\tag{34}$$

Equation (34) is the Arrested Topography Wave (ATW) of Csanady (1978 *JPO*). As a heat diffusion equation, the ATW model simulates a propagating shelf wave arrested by bottom friction ($y$-term $\to 0$ as $\mu\to\infty$). As such, only solutions diffusing in the direction of propagating shelf waves make physical sense.

At the coastal boundary, the longshore surface height setup is balanced by the longshore windstress, which is related to the cross-shore surface height gradient through bottom friction and geostrophy. The longshore surface height gradient vanishes as $h\to0$ at the coast.

$$\frac{\partial\eta}{\partial x}(0, y) = \frac{f}{\mu}\left(\frac{\tau_y}{g} - h\frac{\partial\eta}{\partial y}\right) = q_0 \tag{35}$$

If we orient our solution to $x\to-\infty$ offshore such that $\frac{\partial h}{\partial x} = -s$, then equation (34) is solvable. Equation (35) is analogous to a constant heat flux boundary condition. The solution is given by Carslaw and Jaeger 1959 (p. 112)

$$\eta(x, y) = \frac{\kappa q_0y}{L} + q_0L\left\{\frac{3(x + L)^2 - L^2}{6L^2} - \frac{2}{\pi^2}\sum_{n=1}^\infty\frac{(-1)^n}{n^2}\exp\left(\frac{-\kappa n^2\pi^2y}{L^2}\right)\cos\left(\frac{n\pi(x + L)}{L}\right)\right\} \tag{36}$$

which, as $y\to\infty$, reduces to

$$\eta(x, y) = \frac{\kappa q_0y}{L} + q_0L\frac{3(x + L)^2 - L^2}{6L^2} \tag{37}$$

```python
# Barotropic ATW solution forced by windstress at coast

# Constants and terms
L = 1e3        # [m]
tau_y = -1e-4  # [m2 s-2]
mu = 1e-2      # [s-1]
s = 1          # [dimensionless]
dL = L*1e-2
kappa = mu / (f * s)
q_0 = tau_y * f / (mu * g)

# Coordinates
xi = np.arange(-L, 0, dL)
yi = np.arange(0, L, dL)
x, y = np.meshgrid(xi, yi)

# Solution
eta = kappa * q_0 * y / L + q_0 * L * (3 * (x + L)**2 - L**2) / (6 * L**2)
u_a = -1 / (f) * (tau_y - mu * g / f * np.gradient(eta)[1] / dL)

# Plot
fig, ax = plt.subplots(1, 1, figsize=(10, 10))
ax.contour(xi/L, yi/L, eta, colors='k')
ax.quiver(xi[::5]/L, yi[::5]/L, u_a[::5, ::5], 0, scale=50)
for tick in np.arange(0, 1, 0.01):
    ax.plot([0, 0.01], [tick, tick+0.01], 'k-', clip_on=False)
ax.set_xlabel('$\longleftarrow$ $X/L$')
ax.set_ylabel('$\longleftarrow$ $Y/L$')
ax.xaxis.set_ticks([-1, 0])
ax.yaxis.set_ticks([0, 1])
ax.xaxis.set_ticklabels(['$-L$', 0])
ax.yaxis.set_ticklabels([0, '$L$'])
ax.tick_params(direction='out', pad=8)
ax.set_xlim([0, -1])
ax.set_ylim([1, 0])
ax.text(0.02, 0.05, 'Low $\eta$', transform=ax.transAxes)
ax.text(0.85, 0.9, 'High $\eta$', transform=ax.transAxes)
ax.text(0.03, 0.46, '$\\tau_y$', transform=ax.transAxes)
ax.arrow(0.04, 0.5, 0, 0.1, transform=ax.transAxes, head_width=0.01, facecolor='k')
ax.set_title('Cross-shelf bottom slope (ATW) solution')

plt.show()
```

The ATW model rotates the cross-shore surface height setup anticlockwise, similar to what we see in the SalishSeaCast pressure field.

**Problem**

The force balance normal to a coastline is geostrophic. This orientation changes near a cape, but since the $x$ and $y$ directions are inherent to the model, it's not clear how to accomodate the new orientation near the cape.

***

### ATW Relaxation

Three schemes:

Centered difference

$$r_{i, j}^{(n)} = \frac{\eta_{i, j+1}^{(n)} - \eta_{i, j-1}^{(n)}}{2\Delta y} - \kappa\frac{\eta_{i+1, j}^{(n)} - 2\eta_{i, j}^{(n)} + \eta_{i-1, j}^{(n)}}{\Delta x^2}$$

$$\eta_{i, j}^{(n+1)} = \eta_{i, j}^{(n)} - \frac{\mu\Delta x^2}{2\kappa}r_{i, j}^{(n)}$$

Upstream Euler

$$r_{i, j}^{(n)} = \frac{\eta_{i, j+1}^{(n)} - \eta_{i, j}^{(n)}}{\Delta y} - \kappa\frac{\eta_{i+1, j}^{(n)} - 2\eta_{i, j}^{(n)} + \eta_{i-1, j}^{(n)}}{\Delta x^2}$$

$$\eta_{i, j}^{(n+1)} = \eta_{i, j}^{(n)} - \frac{\mu}{\left(\frac{2\kappa}{\Delta x} - 1\right)}r_{i, j}^{(n)}$$

Downstream Euler

$$r_{i, j}^{(n)} = \frac{\eta_{i, j}^{(n)} - \eta_{i, j-1}^{(n)}}{\Delta y} - \kappa\frac{\eta_{i+1, j}^{(n)} - 2\eta_{i, j}^{(n)} + \eta_{i-1, j}^{(n)}}{\Delta x^2}$$

$$\eta_{i, j}^{(n+1)} = \eta_{i, j}^{(n)} - \frac{\mu}{\left(\frac{2\kappa}{\Delta x} + 1\right)}r_{i, j}^{(n)}$$

Only the downstream Euler works.

```python
# Find phi by relaxation

# Parameters

M = eta.shape[0] # matrix size
Cy = int(M/2)
Cx = int(2*M/3)
mu = 1           # SOR convergence parameter
TOL = 1e-4       # Convergence tolerance

# Allocate arrays
eta_next = copy.deepcopy(eta)
res = np.zeros(eta.shape)

# Make figure array
fig, axs = plt.subplots(1, 3, figsize=(17, 6))

# Plot 3 max iterations (N)
for ax, N in zip(axs, [100, 1000, 10000]):

    # Reset eta (equation 37)
    eta = kappa * q_0 * y / L + q_0 * L * (3 * (x + L)**2 - L**2) / (6 * L**2)

    # Relaxation loop
    for n in range(N):
        for i in range(1, M-1): # Longshore step
            for j in range(1, M-1): # Cross-shore step
                #Downstream Euler
                res[i, j] = (eta[i, j] - eta[i, j-1]) / dL - kappa * (eta[i+1, j] - 2 * eta[i, j] + eta[i-1, j]) / dL**2
                eta_next[i, j] = eta[i, j] - mu / (2 * kappa / dL + 1) * res[i, j]
        eta = eta_next
        #eta[Cy, Cx:] = eta[Cy, -1]  # Reset eta along cape equal to eta at coast
        if dL**2 * np.max(abs(res)) / np.max(abs(eta)) < TOL:
            break

    # Plot results
    ax.contour(xi/L, yi/L, eta, colors='k')
    ax.set_xlim([0, -1])
    ax.set_ylim([1, 0])
```

```python

```

```python

```

```python

```

```python

```

```python
def make_domain_diagram():
    """Quick plotting code for a domain diagram
    """

    # Make figure
    fig, axs = plt.subplots(1, 2, figsize=(17, 8))

    for ax in axs:
        ax.set_xlim([0, 1])
        ax.set_ylim([0, 1])
        ax.set_xlabel('X')
        ax.xaxis.set_ticks([0, 0.2, 1])
        ax.xaxis.set_ticklabels(['E', 0, '$-\infty$'])
        ax.tick_params(direction='out', pad=8)

    # Define layers and regions
    axs[0].plot([0.2, 0.2], [0, 1], 'k--')
    axs[1].plot([0.2, 0.2], [0.5, 1], 'k--')
    axs[1].plot([0, 1], [0.7, 0.7], 'k-')
    axs[1].plot([0, 0.2, 1], [0.5, 0.5, 0], 'k-')

    # Add coastline ticks
    for tick in np.arange(0, 1, 0.02):
        axs[0].plot([-0.01, 0], [tick, tick+0.01], 'k-', clip_on=False)
    for tick in np.arange(0.5, 1, 0.02):
        axs[1].plot([-0.01, 0], [tick, tick+0.01], 'k-', clip_on=False)
    for tick in np.arange(0, 0.2, 0.02):
        axs[1].plot([tick, tick+0.01], [0.49, 0.5], 'k-', clip_on=False)
    for tick in np.arange(0.2, 1, 0.02):
        axs[1].plot([tick, tick+0.01], [0.61-tick*5/8, 0.615-tick*5/8], 'k-')

    # Add labels
    axs[0].set_ylabel('Y')
    axs[0].yaxis.set_ticks([0, 1])
    axs[0].yaxis.set_ticklabels(['$\infty$', 0])
    axs[0].set_title('Plan view')
    axs[1].yaxis.set_ticks([0.5, 0.7, 1])
    axs[1].yaxis.set_ticklabels(['H$_L$', 'H$_U$', 0])
    axs[1].set_title('Side view')

    return fig, axs
```

```python
fig, ax = make_domain_diagram()
```

#### Relaxation solution

The discrete residual of equation (13) is equal to the 5 point stencil

$$r_{i, j}^{(n)} = -\frac{R^2}{d^2}\left(\phi_{i+1, j}^{(n)} + \phi_{i-1, j}^{(n)} + \phi_{i, j+1}^{(n)} + \phi_{i, j-1}^{(n)}\right) + \left(\frac{4R^2}{d^2} + 1\right)\phi_{i, j}^{(n)} \tag{17}$$

We apply the SOR relaxation to $\eta_{i, j}$

$$\phi_{i, j}^{(n+1)} = \phi_{i, j}^{(n)} - \mu\frac{1}{\frac{4R^2}{d^2} + 1}r_{i, j}^{(n)} \tag{18}$$

with convergence criteria

$$d^2\frac{\left|\left|r_{i, j}^{(n)}\right|\right|_\infty}{\left|\left|\phi_{i, j}^{(n)}\right|\right|_\infty} < TOL \hspace{0.5cm} \text{where} \hspace{0.5cm} \left|\left|r_{i, j}^{(n)}\right|\right|_\infty = \max_{i, j}\left|r_{i, j}^{(n)}\right| \tag{19}$$

```python
# Find phi by relaxation

# Parameters
M = 100         # matrix size
N = 1000        # max iterations
d = int(L / M)  # grid space
mu = 1.5        # SOR convergence parameter
TOL = 1e-4      # Convergence tolerance
k = 5e-4

# Indices
x = np.arange(M) / (M - 1) * L
y = (np.arange(M) / (M - 1) - 1) * L

# Allocate arrays
phi = np.zeros((M, M))
r = np.zeros((M, M))

# Calculate eta_bar
eta_bar = calc_eta_bar(x, H_s, H_d, S, E, R, q_0, f)

# Boundaries
phi[-1, :] = eta_bar[0] - eta_bar
phi[:, -1] = (eta_bar[0] - eta_bar[-1]) * np.exp(k * y)

# Loop
for n in range(N):
    for i in range(1, M-1):
        for j in range(1, M-1):
            res[i, j] = -R**2 / d**2 * (phi[i+1, j] + phi[i-1, j] + phi[i, j+1] + phi[i, j-1]) + (4 * R**2 / d**2 + 1) * phi[i, j]
            phi[i, j] = phi[i, j] - mu / (4 * R**2 / d**2 + 1) * res[i, j]
    if d**2 * np.max(abs(res)) / np.max(abs(phi)) < TOL:
        break
```


Validation of the NN code generator for the 2D diffusion equation

O. Pannekoucke

$   %\newcommand{\pde}{\partial}
    %\newcommand{\pdt}{\partial_t}
    %\newcommand{\pdx}{\partial_x}
    \newcommand{\bu}{\bar u}
    \newcommand{\eps}{\varepsilon}$

 Objectifs

 * Definition of the 2D diffusion equation by using `sympy`
 * Computation of the numerical solution of the 2D diffusion using a NN

---

Contents

  1. [Introduction](#intro)
  1. [Dynamics](#model)
  1. [Numerical code for the resolution](#code)
  1. [Numerical application](#num)
  1. [Conclusion](#conclusion)
---

## Introduction

The aim is to compute the solution of the diffusion equation given by
$$\partial_t u = \partial_{x^i}\left(\kappa_{ij}\partial_{x^j} u \right),$$
where $\kappa$  is a field of diffusion tensors.

## Dynamics

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

**Set the diffusion equation**

```python
from sympy import Function, symbols, Derivative
from pdenetgen import Eq, NNModelBuilder

# Defines the diffusion equation using sympy
t, x, y = symbols('t x y')
u = Function('u')(t,x,y)
kappa11 = Function('\\kappa_{11}')(x,y)
kappa12 = Function('\\kappa_{12}')(x,y)
kappa22 = Function('\\kappa_{22}')(x,y)

diffusion_2D = Eq(Derivative(u,t),
  Derivative(kappa11*Derivative(u,x)+
             kappa12*Derivative(u,y),x)+
  Derivative(kappa12*Derivative(u,x)+
             kappa22*Derivative(u,y),y)).doit()

# Defines the neural network code generator
diffusion_nn_builder = NNModelBuilder(diffusion_2D,
        class_name="NNDiffusion2DHeterogeneous")

# Renders the neural network code
exec(diffusion_nn_builder.code)

# Create a 2D Diffusion model
diffusion_model = NNDiffusion2DHeterogeneous()
```

    Warning: function `kappa_12` has to be set
    Warning: function `kappa_22` has to be set
    Warning: function `kappa_11` has to be set

**Sample of NN code generated**
```python
# Example of computation of a derivative
kernel_Du_x_o1 = np.asarray([[0.0,-1/(2*self.dx[self.coordinates.index('x')]),0.0],
 [0.0,0.0,0.0],
 [0.0,1/(2*self.dx[self.coordinates.index('x')]),0.0]]).reshape((3, 3)+(1,1))
Du_x_o1 = DerivativeFactory((3, 3),kernel=kernel_Du_x_o1,name='Du_x_o1')(u)

# Computation of trend_u
mul_0 = keras.layers.multiply([Dkappa_11_x_o1,Du_x_o1],name='MulLayer_0')
mul_1 = keras.layers.multiply([Dkappa_12_x_o1,Du_y_o1],name='MulLayer_1')
mul_2 = keras.layers.multiply([Dkappa_12_y_o1,Du_x_o1],name='MulLayer_2')
mul_3 = keras.layers.multiply([Dkappa_22_y_o1,Du_y_o1],name='MulLayer_3')
mul_4 = keras.layers.multiply([Du_x_o2,kappa_11],name='MulLayer_4')
mul_5 = keras.layers.multiply([Du_y_o2,kappa_22],name='MulLayer_5')
mul_6 = keras.layers.multiply([Du_x_o1_y_o1,kappa_12],name='MulLayer_6')
sc_mul_0 = keras.layers.Lambda(lambda x: 2.0*x,name='ScalarMulLayer_0')(mul_6)
trend_u = keras.layers.add([mul_0,mul_1,mul_2,mul_3,mul_4,mul_5,sc_mul_0],name='AddLayer_0')
```

```python
print(diffusion_nn_builder.code)
```

    from pdenetgen.model import Model
    import numpy as np
    import tensorflow.keras as keras
    from pdenetgen.symbolic.nn_builder import DerivativeFactory, TrainableScalarLayerFactory

    class NNDiffusion2DHeterogeneous(Model):

        # Prognostic functions (sympy functions):
        prognostic_functions = (
                'u',    # Write comments on the function here
            )

        # Spatial coordinates
        coordinates = (
                'x',    # Write comments on the coordinate here
                'y',    # Write comments on the coordinate here
            )

        # Set constant functions
        constant_functions = (
                'kappa_12',    # Writes comment on the constant function here
                'kappa_22',    # Writes comment on the constant function here
                'kappa_11',    # Writes comment on the constant function here
            )

        def __init__(self, shape=None, lengths=None, **kwargs):

            super().__init__() # Time scheme is set from Model.__init__()

            #---------------------------------
            # Set index array from coordinates
            #---------------------------------

            # a) Set shape
            shape = len(self.coordinates)*(100,) if shape is None else shape
            if len(shape)!=len(self.coordinates):
                raise ValueError(f"len(shape) {len(shape)} is different from len(coordinates) {len(self.coordinates)}")
            else:
                self.shape = shape

            # b) Set input shape for coordinates
            self.input_shape_x = shape[0]
            self.input_shape_y = shape[1]

            # c) Set lengths
            lengths = len(self.coordinates)*(1.0,) if lengths is None else lengths
            if len(lengths)!=len(self.coordinates):
                raise ValueError(f"len(lengths) {len(lengths)} is different from len(coordinates) {len(self.coordinates)}")
            else:
                self.lengths = lengths

            # d) Set indexes
            self._index = {}
            for k,coord in enumerate(self.coordinates):
                self._index[(coord,0)] = np.arange(self.shape[k], dtype=int)

            # Set x/dx
            #-------------
            self.dx = tuple(length/shape for length, shape in zip(self.lengths, self.shape))
            self.x = tuple(self.index(coord,0)*dx for coord, dx in zip(self.coordinates, self.dx))
            self.X = np.meshgrid(*self.x)

            #-----------------------
            # Set constant functions
            #-----------------------

            # Set a default nan value for constants
            self.kappa_12 = np.nan # @@ set constant value @@
            self.kappa_22 = np.nan # @@ set constant value @@
            self.kappa_11 = np.nan # @@ set constant value @@

            # Set constant function values from external **kwargs (when provided)
            for key in kwargs:
                if key in self.constant_functions:
                    setattr(self, key, kwargs[key])

            # Alert when a constant is np.nan
            for function in self.constant_functions:
                if getattr(self, function) is np.nan:
                    print(f"Warning: function `{function}` has to be set")

            # Set NN models
            self._trend_model = None
            self._exogenous_model = None

        def index(self, coord, step:int):
            """ Return int array of shift index associated with coordinate `coord` for shift `step` """
            # In this implementation, indexes are memory saved in a dictionary, feed at runtime
            if (coord,step) not in self._index:
                self._index[(coord,step)] = (self._index[(coord,0)]+step)%self.shape[self.coordinates.index(coord)]
            return self._index[(coord,step)]

        def _make_trend_model(self):
            """ Generate the NN used to compute the trend of the dynamics """
            # Alias for constant functions
            #-----------------------------
            kappa_12 = self.kappa_12
            if kappa_12 is np.nan:
                raise ValueError("Constant function 'kappa_12' is not set")
            kappa_22 = self.kappa_22
            if kappa_22 is np.nan:
                raise ValueError("Constant function 'kappa_22' is not set")
            kappa_11 = self.kappa_11
            if kappa_11 is np.nan:
                raise ValueError("Constant function 'kappa_11' is not set")

            # Set input layers
            #------------------
            # Set Alias for coordinate input shapes
            input_shape_x = self.input_shape_x
            input_shape_y = self.input_shape_y

            # Set input shape for prognostic functions
            u = keras.layers.Input(shape =(input_shape_x,input_shape_y,1,))

            # Set input shape for constant functions
            kappa_12 = keras.layers.Input(shape =(input_shape_x,input_shape_y,1,))
            kappa_22 = keras.layers.Input(shape =(input_shape_x,input_shape_y,1,))
            kappa_11 = keras.layers.Input(shape =(input_shape_x,input_shape_y,1,))

            # Keras code

            # 2) Implementation of derivative as ConvNet
            # Compute derivative
            #-----------------------
            #
            #  Warning: might be modified to fit appropriate boundary conditions.
            #
            kernel_Dkappa_11_x_o1 = np.asarray([[0.0,-1/(2*self.dx[self.coordinates.index('x')]),0.0],
             [0.0,0.0,0.0],
             [0.0,1/(2*self.dx[self.coordinates.index('x')]),0.0]]).reshape((3, 3)+(1,1))
            Dkappa_11_x_o1 = DerivativeFactory((3, 3),kernel=kernel_Dkappa_11_x_o1,name='Dkappa_11_x_o1')(kappa_11)

            kernel_Du_x_o1_y_o1 = np.asarray([[1/(4*self.dx[self.coordinates.index('x')]*self.dx[self.coordinates.index('y')]),
              0.0,
              -1/(4*self.dx[self.coordinates.index('x')]*self.dx[self.coordinates.index('y')])],
             [0.0,0.0,0.0],
             [-1/(4*self.dx[self.coordinates.index('x')]*self.dx[self.coordinates.index('y')]),
              0.0,
              1/(4*self.dx[self.coordinates.index('x')]*self.dx[self.coordinates.index('y')])]]).reshape((3, 3)+(1,1))
            Du_x_o1_y_o1 = DerivativeFactory((3, 3),kernel=kernel_Du_x_o1_y_o1,name='Du_x_o1_y_o1')(u)

            kernel_Du_y_o2 = np.asarray([[0.0,0.0,0.0],
             [self.dx[self.coordinates.index('y')]**(-2),
              -2/self.dx[self.coordinates.index('y')]**2,
              self.dx[self.coordinates.index('y')]**(-2)],
             [0.0,0.0,0.0]]).reshape((3, 3)+(1,1))
            Du_y_o2 = DerivativeFactory((3, 3),kernel=kernel_Du_y_o2,name='Du_y_o2')(u)

            kernel_Dkappa_12_y_o1 = np.asarray([[0.0,0.0,0.0],
             [-1/(2*self.dx[self.coordinates.index('y')]),0.0,
              1/(2*self.dx[self.coordinates.index('y')])],
             [0.0,0.0,0.0]]).reshape((3, 3)+(1,1))
            Dkappa_12_y_o1 = DerivativeFactory((3, 3),kernel=kernel_Dkappa_12_y_o1,name='Dkappa_12_y_o1')(kappa_12)

            kernel_Dkappa_12_x_o1 = np.asarray([[0.0,-1/(2*self.dx[self.coordinates.index('x')]),0.0],
             [0.0,0.0,0.0],
             [0.0,1/(2*self.dx[self.coordinates.index('x')]),0.0]]).reshape((3, 3)+(1,1))
            Dkappa_12_x_o1 = DerivativeFactory((3, 3),kernel=kernel_Dkappa_12_x_o1,name='Dkappa_12_x_o1')(kappa_12)

            kernel_Du_x_o1 = np.asarray([[0.0,-1/(2*self.dx[self.coordinates.index('x')]),0.0],
             [0.0,0.0,0.0],
             [0.0,1/(2*self.dx[self.coordinates.index('x')]),0.0]]).reshape((3, 3)+(1,1))
            Du_x_o1 = DerivativeFactory((3, 3),kernel=kernel_Du_x_o1,name='Du_x_o1')(u)

            kernel_Du_x_o2 = np.asarray([[0.0,self.dx[self.coordinates.index('x')]**(-2),0.0],
             [0.0,-2/self.dx[self.coordinates.index('x')]**2,0.0],
             [0.0,self.dx[self.coordinates.index('x')]**(-2),0.0]]).reshape((3, 3)+(1,1))
            Du_x_o2 = DerivativeFactory((3, 3),kernel=kernel_Du_x_o2,name='Du_x_o2')(u)

            kernel_Dkappa_22_y_o1 = np.asarray([[0.0,0.0,0.0],
             [-1/(2*self.dx[self.coordinates.index('y')]),0.0,
              1/(2*self.dx[self.coordinates.index('y')])],
             [0.0,0.0,0.0]]).reshape((3, 3)+(1,1))
            Dkappa_22_y_o1 = DerivativeFactory((3, 3),kernel=kernel_Dkappa_22_y_o1,name='Dkappa_22_y_o1')(kappa_22)

            kernel_Du_y_o1 = np.asarray([[0.0,0.0,0.0],
             [-1/(2*self.dx[self.coordinates.index('y')]),0.0,
              1/(2*self.dx[self.coordinates.index('y')])],
             [0.0,0.0,0.0]]).reshape((3, 3)+(1,1))
            Du_y_o1 = DerivativeFactory((3, 3),kernel=kernel_Du_y_o1,name='Du_y_o1')(u)

            # 3) Implementation of the trend as NNet

            #
            # Computation of trend_u
            #
            mul_0 = keras.layers.multiply([Dkappa_11_x_o1,Du_x_o1],name='MulLayer_0')
            mul_1 = keras.layers.multiply([Dkappa_12_x_o1,Du_y_o1],name='MulLayer_1')
            mul_2 = keras.layers.multiply([Dkappa_12_y_o1,Du_x_o1],name='MulLayer_2')
            mul_3 = keras.layers.multiply([Dkappa_22_y_o1,Du_y_o1],name='MulLayer_3')
            mul_4 = keras.layers.multiply([Du_x_o2,kappa_11],name='MulLayer_4')
            mul_5 = keras.layers.multiply([Du_y_o2,kappa_22],name='MulLayer_5')
            mul_6 = keras.layers.multiply([Du_x_o1_y_o1,kappa_12],name='MulLayer_6')
            sc_mul_0 = keras.layers.Lambda(lambda x: 2.0*x,name='ScalarMulLayer_0')(mul_6)
            trend_u = keras.layers.add([mul_0,mul_1,mul_2,mul_3,mul_4,mul_5,sc_mul_0],name='AddLayer_0')

            # 4) Set 'input' of model
            inputs = [
                    # Prognostic functions
                    u,
                    # Constant functions
                    kappa_12,kappa_22,kappa_11,
                ]

            # 5) Set 'outputs' of model
            outputs = [
                trend_u,
                ]

            model = keras.models.Model(inputs=inputs, outputs=outputs)
            #model.trainable = False
            self._trend_model = model

        def trend(self, t, state):
            """ Trend of the dynamics """

            if self._trend_model is None:
                self._make_trend_model()

            # Init output state with pointer on data
            #-------------------------------------------

            #   a) Set the output array
            dstate = np.zeros(state.shape)

            #   b) Set pointers on output array `dstate` for the computation of the physical trend (alias only).
            du = dstate[0]

            # Load physical functions from state
            #------------------------------------
            u = state[0]

            # Compute the trend value from model.predict
            #-------------------------------------------
            inputs = [
                # Prognostic functions
                    u,
                    # Constant functions
                    self.kappa_12,
                    self.kappa_22,
                    self.kappa_11,
                    ]

            dstate = self._trend_model.predict( inputs )

            if not isinstance(dstate,list):
                dstate = [dstate]

            return np.array(dstate)

        def _make_dynamical_trend(self):
            """
            Computation of a trend model so to be used in a time scheme (as solving a dynamical system or an ODE)

            Description:
            ------------

            In the present implementation, the inputs of the trend `self._trend_model` is a list of fields, while
            entry of a time-scheme is a single array which contains all fields.

            The aims of `self._dynamical_trend` is to produce a Keras model which:
            1. takes a single array as input
            2. extract the `self._trend_model` input list from the input array
            3. compute the trends from `self._trend_model`
            4. outputs the trends as a single array

            Explaination of the code:
            -------------------------

            Should implement a code as the following, that is valid for the PKF-Burgers

            def _make_dynamical_trend(self):

                if self._trend_model is None:
                    self._make_trend_model()

                # 1. Extract the input of the model

                # 1.1 Set the input as an array
                state = keras.layers.Input(shape=(3,self.input_shape_x,1))

                # 1.2 Extract each components of the state
                u = keras.layers.Lambda(lambda x : x[:,0,:,:])(state)
                V = keras.layers.Lambda(lambda x : x[:,1,:,:])(state)
                nu_u_xx = keras.layers.Lambda(lambda x : x[:,2,:,:])(state)

                # 2. Compute the trend
                trend_u, trend_V, trend_nu = self._trend_model([u,V,nu_u_xx])

                # 3. Outputs the trend as a single array

                # 3.1 Reshape trends
                trend_u = keras.layers.Reshape((1,self.input_shape_x,1))(trend_u)
                trend_V = keras.layers.Reshape((1,self.input_shape_x,1))(trend_V)
                trend_nu = keras.layers.Reshape((1,self.input_shape_x,1))(trend_nu)

                # 3.2 Concatenates all trends
                trends = keras.layers.Concatenate(axis=1)([trend_u,trend_V,trend_nu])

                # 4. Set the dynamical_trend model
                self._dynamical_trend = keras.models.Model(inputs=state,outputs=trends)
            """

            if self._trend_model is None:
                self._make_trend_model()

            for exclude_case in ['constant_functions','exogenous_functions']:
                if hasattr(self,exclude_case):
                    raise NotImplementedError(f'Design of dynamical_model with {exclude_case} is not implemented')

            # Case 1 --  corresponds to the _trend_model if input is a single field
            if not isinstance(self._trend_model.input_shape, list):
                self._dynamical_trend = self._trend_model
                return

            # Case 2 -- Case where multiple list is used

            # 1. Extract the input of the model

            # 1.1 Set the input as an array
            """ from PKF-Burgers code:
            state = keras.layers.Input(shape=(3,self.input_shape_x,1))
            """

            # 1.1.1 Compute the input_shape from _trend_model
            shapes = []
            dimensions  = []
            for shape in self._trend_model.input_shape:
                shape = shape[1:] # Exclude batch_size (assumed to be at first)
                shapes.append(shape)
                dimensions.append(len(shape)-1)

            max_dimension = max(dimensions)
            if max_dimension!=1:
                if 1 in dimensions:
                    raise NotImplementedError('1D fields incompatible with 2D/3D fields')

            # todo: add test to check compatibility of shapes!!!!

            if max_dimension in [1,2]:
                input_shape = (len(shapes),)+shapes[0]
            elif max_dimension==3:

                # a. check the size of 2D fields: this is given by the first 2D field.
                for shape, dimension in zip(shapes, dimensions):
                    if dimension==2:
                        input_shape_2D = shape
                        break

                # b. Compute the numbers of 2D fields: this corresponds to the number of 3D layers and the number of 2D fields.

                for shape, dimension in zip(shapes, dimensions):
                    if dimension==2:
                        nb_outputs += 1
                    else:
                        nb_outputs += shape[0]

                input_shape = (nb_outputs,)+input_shape_2D

            # 1.1.2 Init the state of the dynamical_trend
            state = keras.layers.Input(shape=input_shape)

            # 1.2 Extract each components of the state
            """ From PKF-Burgers code:

            u = keras.layers.Lambda(lambda x : x[:,0,:,:])(state)
            V = keras.layers.Lambda(lambda x : x[:,1,:,:])(state)
            nu_u_xx = keras.layers.Lambda(lambda x : x[:,2,:,:])(state)

            inputs = [u, V, nu_u_xx]
            """

            def get_slice(dimension, k):
                def func(x):
                    if dimension == 1:
                        return x[:,k,:,:]
                    elif dimension == 2:
                        return x[:,k,:,:]
                return func

            def get_slice_3d(start,end):
                def func(x):
                    return x[:,start:end,:,:,:]
                return func

            inputs = []
            if max_dimension in [1,2]:
                for k in range(len(shapes)):
                    inputs.append(keras.layers.Lambda(get_slice(max_dimension,k))(state))
                    #if max_dimension==1:
                    #    inputs.append(keras.layers.Lambda(lambda x : x[:,k,:,:])(state))
                    #
                    #if max_dimension==2:
                    #    inputs.append(keras.layers.Lambda(lambda x : x[:,k,:,:,:])(state))
            else:
                k=0
                for shape, dimension in zip(shapes, dimensions):
                    if dimension==2:
                        #inputs.append(keras.layers.Lambda(lambda x : x[:,k,:,:,:])(state))
                        inputs.append(keras.layers.Lambda(get_slice(dimension,k))(state))
                        k += 1
                    if dimension==3:
                        start = k
                        end = start+shape[0]
                        inputs.append(keras.layers.Lambda(get_slice_3d(start,end))(state))
                        k = end

            # 2. Compute the trend
            """ From PKF-Burgers code
            trend_u, trend_V, trend_nu = self._trend_model([u,V,nu_u_xx])
            """
            trends = self._trend_model(inputs)

            # 3. Outputs the trend as a single array

            # 3.1 Reshape trends
            """ from PKF-Burgers code
            trend_u = keras.layers.Reshape((1,self.input_shape_x,1))(trend_u)
            trend_V = keras.layers.Reshape((1,self.input_shape_x,1))(trend_V)
            trend_nu = keras.layers.Reshape((1,self.input_shape_x,1))(trend_nu)
            """

            reshape_trends = []
            for trend, dimension in zip(trends, dimensions):

                #shape = tuple(dim.value for dim in trend.shape[1:])
                # update from keras -> tensorflow.keras
                shape = tuple(dim for dim in trend.shape[1:])

                if dimension==1 or dimension==2:
                    # for 1D fields like (128,1) transform into (1,128,1)
                    # for 2D fields like (128,128,1) transform into (1,128,128,1)
                    shape = (1,)+shape
                elif dimension==3:
                    # 3D fields can be compated: two fields (36,128,128,1) become the single field (72,128,128,1)
                    pass
                else:
                    raise NotImplementedError

                reshape_trends.append(keras.layers.Reshape(shape)(trend))

            # 3.2 Concatenates all trends
            """ From PKF-Burgers code:
            trends = keras.layers.Concatenate(axis=1)([trend_u,trend_V,trend_nu])
            """
            trends = keras.layers.Concatenate(axis=1)(reshape_trends)

            # 2.5 Compute the model
            self._dynamical_trend = keras.models.Model(inputs=state,outputs=trends)

## Numerical application

### Definition of the domain of computation

The domain is the bi-periodic sqare $[0,1)\times [0,1)$

### Set the numerical NN model

```python
domain = diffusion_model
```

**Set initial fields**

```python
import numpy as np
```

```python
dx, dy = diffusion_model.dx

# Set a dirac at the center of the domain.
U = np.zeros(diffusion_model.shape)
U[diffusion_model.shape[0]//2, diffusion_model.shape[0]//2] = 1./(dx*dy)

```

```python
diffusion_model.shape
```

```python
X = np.asarray(diffusion_model.X)
k = np.asarray([1,2])
X = np.moveaxis(X,0,2)
print(X.shape)

np.linalg.norm(X@k -k[0]*diffusion_model.X[0]-k[1]*diffusion_model.X[1])
```

**Set constants and time step**

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

```python
time_scale = 1.
#
# Construction du tenseur de diffusion
#

# a) Définition des composantes principales
lx, ly = 10*dx, 5*dy
kappa_11 = lx**2/time_scale
kappa_22 = ly**2/time_scale

# b) Construction d'un matrice de rotation
R = lambda theta : np.array([[np.cos(theta), -np.sin(theta)],[np.sin(theta), np.cos(theta)]])

# c) Spectre du tenseur de référence
D = np.diag([kappa_11,kappa_22])

# d) Set diffusion tensors field
diffusion_model.kappa_11 = np.zeros(diffusion_model.shape)
diffusion_model.kappa_12 = np.zeros(diffusion_model.shape)
diffusion_model.kappa_22 = np.zeros(diffusion_model.shape)

X = np.moveaxis(np.asarray(diffusion_model.X),0,2)
k = 2*np.pi*np.array([2,3])

theta = np.pi/3*np.cos(X@k)
#plt.contourf(*num_model.x, theta)

for i in range(diffusion_model.shape[0]):
    for j in range(diffusion_model.shape[1]):
        lR = R(theta[i,j])
        nu = lR@np.diag([kappa_11,kappa_22])@lR.T
        diffusion_model.kappa_11[i,j] = nu[0,0]
        diffusion_model.kappa_12[i,j] = nu[0,1]
        diffusion_model.kappa_22[i,j] = nu[1,1]

diffusion_model.kappa_11 = diffusion_model.kappa_11.reshape((1,100,100,1))
diffusion_model.kappa_12 = diffusion_model.kappa_12.reshape((1,100,100,1))
diffusion_model.kappa_22 = diffusion_model.kappa_22.reshape((1,100,100,1))
#
# Calcul du pas de temps adapté au problème
#
dt = np.min([dx**2/kappa_11, dy**2/kappa_22])

CFL = 1/6
diffusion_model._dt = CFL * dt
print('time step:', diffusion_model._dt)
```

    time step: 0.0016666666666666663

**Illustrates trend at initial condition**

```python
def plot(field):
    plt.contourf(*diffusion_model.x, field.T)
```

```python
state0 = U.copy().reshape((1,1)+diffusion_model.shape+(1,))
print(state0.shape)
```

    (1, 1, 100, 100, 1)

```python
import tensorflow.keras as keras
```

```python
dU, = diffusion_model.trend(0,state0)

plot(dU[0].reshape((100,100)))
plt.title('Trend for the diffusion')
```

**Short forecast**

```python
times = diffusion_model.window(time_scale)
#saved_times = times[::100]
saved_times = times
```

```python
diffusion_model.set_time_scheme('euler')
traj = diffusion_model.forecast(times, state0, saved_times)
```

```python
plt.figure(figsize=(12,5))

start, end = [traj[time] for time in [saved_times[0], saved_times[-1]]]

title = ['start', 'end']
for k, state in enumerate([start, end]):
    plt.subplot(121+k)
    plot(state[0].reshape((100,100)))
    plt.title(title[k])

plt.savefig('./figures/NN-diffusion-2D-prediction.pdf')
np.save('nn-diffusion.npy', end)
```

**Comparison with the finite difference solution**

```python
fd_end_solution = np.load('fd-diffusion.npy')
```

```python
np.linalg.norm(fd_end_solution[0] - end[0,0,:,:,0])
```

## Conclusion

In this notebook, the solution of the diffusion equation using a NN code has been presented.
The results are those of the finite-difference solution (not shown here). This validate the NN generator and illustrates how the physical equations can be used for the design of a NN architecture.


## Histograms of Oriented Gradients (HOG)

As we saw with the ORB algorithm, we can use keypoints in images to do keypoint-based matching to detect objects in images. These type of algorithms work great when you want to detect objects that have a lot of consistent internal features that are not affected by the background. For example, these algorithms work well for facial detection because faces have a lot of consistent internal features that don’t get affected by the image background, such as the eyes, nose, and mouth. However, these type of algorithms don’t work so well when attempting to do more general object recognition, say for example, pedestrian detection in images. The reason is that people don’t have consistent internal features, like faces do, because the body shape and style of every person is different (see Fig. 1). This means that every person is going to have a different set of internal features, and so we need something that can more generally describe a person.

Fig. 1. - Pedestrians.

One option is to try to detect pedestrians by their contours instead.  Detecting objects in images by their contours (boundaries) is very challenging because we have to deal with the difficulties brought about by the contrast between the background and the foreground. For example, suppose you wanted to detect a pedestrian in an image that is walking in front of a white building and she is wearing a white coat and black pants (see Fig. 2). We can see in Fig. 2, that since the background of the image is mostly white, the black pants are going to have a very high contrast, but the coat, since it is white as well, is going to have very low contrast. In this case, detecting the edges of pants is going to be easy but detecting the edges of the coat is going to be very difficult. This is where **HOG** comes in. HOG stands for **Histograms of Oriented Gradients** and it was first introduced by Navneet Dalal and Bill Triggs in 2005.

Fig. 2. - High and Low Contrast.

The HOG algorithm works by creating histograms of the distribution of gradient orientations in an image and then normalizing them in a very special way. This special normalization is what makes HOG so effective at detecting the edges of objects even in cases where the contrast is very low. These normalized histograms are put together into a feature vector, known as the HOG descriptor, that can be used to train a machine learning algorithm, such as a Support Vector Machine (SVM), to detect objects in images based on their boundaries (edges). Due to its great success and reliability, HOG has become one of the most widely used algorithms in computer vison for object detection.

In this notebook, you will learn:

* How the HOG algorithm works
* How to use OpenCV to create a HOG descriptor
* How to visualize the HOG descriptor.

# The HOG Algorithm

As its name suggests, the HOG algorithm, is based on creating histograms from the orientation of image gradients. The HOG algorithm is implemented in a series of steps:

1. Given the image of particular object, set a detection window (region of interest) that covers the entire object in the image (see Fig. 3).

2. Calculate the magnitude and direction of the gradient for each individual pixel in the detection window.

3. Divide the detection window into connected *cells* of pixels, with all cells being of the same size (see Fig. 3). The size of the cells is a free parameter and it is usually chosen so as to match the scale of the features that want to be detected. For example, in a 64 x 128 pixel detection window, square cells 6 to 8 pixels wide are suitable for detecting human limbs.

4. Create a Histogram for each cell, by first grouping the gradient directions of all pixels in each cell into a particular number of orientation (angular) bins; and then adding up the gradient magnitudes of the gradients in each angular bin (see Fig. 3). The number of bins in the histogram is a free parameter and it is usually set to 9 angular bins.

5. Group adjacent cells into *blocks* (see Fig. 3). The number of cells in each block is a free parameter and all blocks must be of the same size. The distance between each block (known as the stride) is a free parameter but it is usually set to half the block size, in which case you will get overlapping blocks (*see video below*).  The HOG algorithm has been shown empirically to work better with overlapping blocks.

6. Use the cells contained within each block to normalize the cell histograms in that block (see Fig. 3). If you have overlapping blocks this means that most cells will be normalized with respect to different blocks (*see video below*). Therefore, the same cell may have several different normalizations.

7. Collect all the normalized histograms from all the blocks into a single feature vector called the HOG descriptor.

8. Use the resulting HOG descriptors from many images of the same type of object to train a machine learning algorithm, such as an SVM, to detect those type of objects in images. For example, you could use the HOG descriptors from many images of pedestrians to train an SVM to detect pedestrians in images. The training is done with both positive a negative examples of the object you want detect in the image.

9. Once the SVM has been trained, a sliding window approach is used to try to detect and locate objects in images.  Detecting an object in the image entails finding the part of the image that looks similar to the HOG pattern learned by the SVM.

Fig. 3. - HOG Diagram.

Vid. 1. - HOG Animation.

# Why The HOG Algorithm Works

As we learned above, HOG creates histograms by adding the magnitude of the gradients in particular orientations in localized portions of the image called *cells*.  By doing this we guarantee that stronger gradients will contribute more to the magnitude of their respective angular bin, while the effects of weak and randomly oriented gradients resulting from noise are minimized. In this manner the histograms tell us the dominant gradient orientation of each cell.

### Dealing with contrast

Now, the magnitude of the dominant orientation can vary widely due to variations in local illumination and the contrast between the background and the foreground.

To account for the background-foreground contrast differences, the HOG algorithm tries to detect edges locally.  In order to do this, it defines groups of cells, called **blocks**, and normalizes the histograms using this local group of cells. By normalizing locally, the HOG algorithm can detect the edges in each block very reliably; this is called **block normalization**.

In addition to using block normalization, the HOG algorithm also uses overlapping blocks to increase its performance. By using overlapping blocks, each cell contributes several independent components to the final HOG descriptor, where each component corresponds to a cell being normalized with respect to a different block. This may seem redundant but, it has been shown empirically that by normalizing each cell several times with respect to different local blocks, the performance of the HOG algorithm increases dramatically.

### Loading Images and Importing Resources

The first step in building our HOG descriptor is to load the required packages into Python and to load our image.

We start by using OpenCV to load an image of a triangle tile. Since, the `cv2.imread()` function loads images as BGR we will convert our image to RGB so we can display it with the correct colors. As usual we will convert our BGR image to Gray Scale for analysis.

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

# Set the default figure size
plt.rcParams['figure.figsize'] = [17.0, 7.0]

# Load the image
image = cv2.imread('./images/triangle_tile.jpeg')

# Convert the original image to RGB
original_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

# Convert the original image to gray scale
gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

# Print the shape of the original and gray scale images
print('The original image has shape: ', original_image.shape)
print('The gray scale image has shape: ', gray_image.shape)

# Display the images
plt.subplot(121)
plt.imshow(original_image)
plt.title('Original Image')
plt.subplot(122)
plt.imshow(gray_image, cmap='gray')
plt.title('Gray Scale Image')
plt.show()
```

# Creating The HOG Descriptor

We will be using OpenCV’s `HOGDescriptor` class to create the HOG descriptor. The parameters of the HOG descriptor are setup using the `HOGDescriptor()` function. The parameters of the `HOGDescriptor()` function and their default values are given below:

`cv2.HOGDescriptor(win_size = (64, 128),
                  block_size = (16, 16),
                  block_stride = (8, 8),
                  cell_size = (8, 8),
                  nbins = 9,
                  win_sigma = DEFAULT_WIN_SIGMA,
                  threshold_L2hys = 0.2,
                  gamma_correction = true,
                  nlevels = DEFAULT_NLEVELS)`

Parameters:

* **win_size** – *Size*
Size of detection window in pixels (*width, height*). Defines the region of interest. Must be an integer multiple of cell size.

* **block_size** – *Size*
Block size in pixels (*width, height*). Defines how many cells are in each block. Must be an integer multiple of cell size and it must be smaller than the detection window. The smaller the block the finer detail you will get.

* **block_stride** – *Size*
Block stride in pixels (*horizontal, vertical*). It must be an integer multiple of cell size. The `block_stride` defines the distance between adjecent blocks, for example, 8 pixels horizontally and 8 pixels vertically. Longer `block_strides` makes the algorithm run faster (because less blocks are evaluated) but the algorithm may not perform as well.

* **cell_size** – *Size*
Cell size in pixels (*width, height*). Determines the size fo your cell. The smaller the cell the finer detail you will get.

* **nbins** – *int*
Number of bins for the histograms. Determines the number of angular bins used to make the histograms. With more bins you capture more gradient directions. HOG uses unsigned gradients, so the angular bins will have values between 0 and 180 degrees.

* **win_sigma** – *double*
Gaussian smoothing window parameter. The performance of the HOG algorithm can be improved by smoothing the pixels near the edges of the blocks by applying a Gaussian spatial window to each pixel before computing the histograms.

* **threshold_L2hys** – *double*
L2-Hys (Lowe-style clipped L2 norm) normalization method shrinkage. The L2-Hys method is used to normalize the blocks and it consists of an L2-norm followed by clipping and a renormalization. The clipping limits the maximum value of the descriptor vector for each block to have the value of the given threshold (0.2 by default). After the clipping the descriptor vector is renormalized as described in *IJCV*, 60(2):91-110, 2004.

* **gamma_correction** – *bool*
Flag to specify whether the gamma correction preprocessing is required or not. Performing gamma correction slightly increases the performance of the HOG algorithm.

* **nlevels** – *int*
Maximum number of detection window increases.

As we can see, the  `cv2.HOGDescriptor()`function supports a wide range of parameters. The first few arguments (`block_size, block_stride, cell_size`, and `nbins`) are probably the ones you are most likely to change. The other parameters can be safely left at their default values and you will get good results.

In the code below, we will use the `cv2.HOGDescriptor()`function to set the cell size, block size, block stride, and the number of bins for the histograms of the HOG descriptor. We will then use `.compute(image)`method to compute the HOG descriptor (feature vector) for the given `image`.

```python
# Specify the parameters for our HOG descriptor

# Cell Size in pixels (width, height). Must be smaller than the size of the detection window
# and must be chosen so that the resulting Block Size is smaller than the detection window.
cell_size = (6, 6)

# Number of cells per block in each direction (x, y). Must be chosen so that the resulting
# Block Size is smaller than the detection window
num_cells_per_block = (2, 2)

# Block Size in pixels (width, height). Must be an integer multiple of Cell Size.
# The Block Size must be smaller than the detection window
block_size = (num_cells_per_block[0] * cell_size[0],
              num_cells_per_block[1] * cell_size[1])

# Calculate the number of cells that fit in our image in the x and y directions
x_cells = gray_image.shape[1] // cell_size[0]
y_cells = gray_image.shape[0] // cell_size[1]

# Horizontal distance between blocks in units of Cell Size. Must be an integer and it must
# be set such that (x_cells - num_cells_per_block[0]) / h_stride = integer.
h_stride = 1

# Vertical distance between blocks in units of Cell Size. Must be an integer and it must
# be set such that (y_cells - num_cells_per_block[1]) / v_stride = integer.
v_stride = 1

# Block Stride in pixels (horizantal, vertical). Must be an integer multiple of Cell Size
block_stride = (cell_size[0] * h_stride, cell_size[1] * v_stride)

# Number of gradient orientation bins
num_bins = 9

# Specify the size of the detection window (Region of Interest) in pixels (width, height).
# It must be an integer multiple of Cell Size and it must cover the entire image. Because
# the detection window must be an integer multiple of cell size, depending on the size of
# your cells, the resulting detection window might be slightly smaller than the image.
# This is perfectly ok.
win_size = (x_cells * cell_size[0] , y_cells * cell_size[1])

# Print the shape of the gray scale image for reference
print('\nThe gray scale image has shape: ', gray_image.shape)
print()

# Print the parameters of our HOG descriptor
print('HOG Descriptor Parameters:\n')
print('Window Size:', win_size)
print('Cell Size:', cell_size)
print('Block Size:', block_size)
print('Block Stride:', block_stride)
print('Number of Bins:', num_bins)
print()

# Set the parameters of the HOG descriptor using the variables defined above
hog = cv2.HOGDescriptor(win_size, block_size, block_stride, cell_size, num_bins)

# Compute the HOG Descriptor for the gray scale image
hog_descriptor = hog.compute(gray_image)
```

    The gray scale image has shape:  (250, 250)

    HOG Descriptor Parameters:

    Window Size: (246, 246)
    Cell Size: (6, 6)
    Block Size: (12, 12)
    Block Stride: (6, 6)
    Number of Bins: 9

# Number of Elements In The HOG Descriptor

The resulting HOG Descriptor (feature vector), contains the normalized histograms from all cells from all blocks in the detection window concatenated in one long vector. Therefore, the size of the HOG feature vector will be given by the total number of blocks in the detection window, multiplied by the number of cells per block, times the number of orientation bins:

\begin{equation}
\mbox{total_elements} = (\mbox{total_number_of_blocks})\mbox{ } \times \mbox{ } (\mbox{number_cells_per_block})\mbox{ } \times \mbox{ } (\mbox{number_of_bins})
\end{equation}

If we don’t have overlapping blocks (*i.e.* the `block_stride`equals the `block_size`), the total number of blocks can be easily calculated by dividing the size of the detection window by the block size. However, in the general case we have to take into account the fact that we have overlapping blocks. To find the total number of blocks in the general case (*i.e.* for any `block_stride` and `block_size`), we can use the formula given below:

\begin{equation}
\mbox{Total}_i = \left( \frac{\mbox{block_size}_i}{\mbox{block_stride}_i} \right)\left( \frac{\mbox{window_size}_i}{\mbox{block_size}_i} \right) - \left [\left( \frac{\mbox{block_size}_i}{\mbox{block_stride}_i} \right) - 1 \right]; \mbox{  for  } i = x,y
\end{equation}

Where Total$_x$, is the total number of blocks along the width of the detection window, and Total$_y$, is the total number of blocks along the height of the detection window. This formula for Total$_x$ and Total$_y$, takes into account the extra blocks that result from overlapping. After calculating Total$_x$ and Total$_y$, we can get the total number of blocks in the detection window by multiplying Total$_x$  $\times$ Total$_y$. The above formula can be simplified considerably because the `block_size`, `block_stride`, and `window_size`are all defined in terms of the `cell_size`. By making all the appropriate substitutions and cancelations the above formula reduces to:

\begin{equation}
\mbox{Total}_i = \left(\frac{\mbox{cells}_i - \mbox{num_cells_per_block}_i}{N_i}\right) + 1\mbox{  }; \mbox{  for  } i = x,y
\end{equation}

Where cells$_x$ is the total number of cells along the width of the detection window, and cells$_y$, is the total number of cells along the height of the detection window. And $N_x$ is the horizontal block stride in units of `cell_size` and $N_y$ is the vertical block stride in units of `cell_size`.

Let's calculate what the number of elements for the HOG feature vector should be and check that it matches the shape of the HOG Descriptor calculated above.

```python
# Calculate the total number of blocks along the width of the detection window
tot_bx = np.uint32(((x_cells - num_cells_per_block[0]) / h_stride) + 1)

# Calculate the total number of blocks along the height of the detection window
tot_by = np.uint32(((y_cells - num_cells_per_block[1]) / v_stride) + 1)

# Calculate the total number of elements in the feature vector
tot_els = (tot_bx) * (tot_by) * num_cells_per_block[0] * num_cells_per_block[1] * num_bins

# Print the total number of elements the HOG feature vector should have
print('\nThe total number of elements in the HOG Feature Vector should be: ',
      tot_bx, 'x',
      tot_by, 'x',
      num_cells_per_block[0], 'x',
      num_cells_per_block[1], 'x',
      num_bins, '=',
      tot_els)

# Print the shape of the HOG Descriptor to see that it matches the above
print('\nThe HOG Descriptor has shape:', hog_descriptor.shape)
print()
```

    The total number of elements in the HOG Feature Vector should be:  40 x 40 x 2 x 2 x 9 = 57600

    The HOG Descriptor has shape: (57600, 1)

# Visualizing The HOG Descriptor

We can visualize the HOG Descriptor by plotting the histogram associated with each cell as a collection of vectors. To do this, we will plot each bin in the histogram as a single vector whose magnitude is given by the height of the bin and its orientation is given by the angular bin that its associated with. Since any given cell might have multiple histograms associated with it, due to the overlapping blocks, we will choose to average all the histograms for each cell to produce a single histogram for each cell.

OpenCV has no easy way to visualize the HOG Descriptor, so we have to do some manipulation first in order to visualize it. We will start by reshaping the HOG Descriptor in order to make our calculations easier.  We will then compute the average histogram of each cell and finally we will convert the histogram bins into vectors. Once we have the vectors, we plot the corresponding vectors for each cell in an image.

The code below produces an interactive plot so that you can interact with the figure. The figure contains:
* the grayscale image,
* the HOG Descriptor (feature vector),
* a zoomed-in portion of the HOG Descriptor, and
* the histogram of the selected cell.

**You can click anywhere on the gray scale image or the HOG Descriptor image to select a particular cell**. Once you click on either image a *magenta* rectangle will appear showing the cell you selected. The Zoom Window will show you a zoomed in version of the HOG descriptor around the selected cell; and the histogram plot will show you the corresponding histogram for the selected cell. The interactive window also has buttons at the bottom that allow for other functionality, such as panning, and giving you the option to save the figure if desired. The home button returns the figure to its default value.

**NOTE**: If you are running this notebook in the Udacity workspace, there is around a 2 second lag in the interactive plot. This means that if you click in the image to zoom in, it will take about 2 seconds for the plot to refresh.

```python
%matplotlib notebook

import copy
import matplotlib.patches as patches

# Set the default figure size
plt.rcParams['figure.figsize'] = [9.8, 9]

# Reshape the feature vector to [blocks_y, blocks_x, num_cells_per_block_x, num_cells_per_block_y, num_bins].
# The blocks_x and blocks_y will be transposed so that the first index (blocks_y) referes to the row number
# and the second index to the column number. This will be useful later when we plot the feature vector, so
# that the feature vector indexing matches the image indexing.
hog_descriptor_reshaped = hog_descriptor.reshape(tot_bx,
                                                 tot_by,
                                                 num_cells_per_block[0],
                                                 num_cells_per_block[1],
                                                 num_bins).transpose((1, 0, 2, 3, 4))

# Print the shape of the feature vector for reference
print('The feature vector has shape:', hog_descriptor.shape)

# Print the reshaped feature vector
print('The reshaped feature vector has shape:', hog_descriptor_reshaped.shape)

# Create an array that will hold the average gradients for each cell
ave_grad = np.zeros((y_cells, x_cells, num_bins))

# Print the shape of the ave_grad array for reference
print('The average gradient array has shape: ', ave_grad.shape)

# Create an array that will count the number of histograms per cell
hist_counter = np.zeros((y_cells, x_cells, 1))

# Add up all the histograms for each cell and count the number of histograms per cell
for i in range (num_cells_per_block[0]):
    for j in range(num_cells_per_block[1]):
        ave_grad[i:tot_by + i,
                 j:tot_bx + j] += hog_descriptor_reshaped[:, :, i, j, :]

        hist_counter[i:tot_by + i,
                     j:tot_bx + j] += 1

# Calculate the average gradient for each cell
ave_grad /= hist_counter

# Calculate the total number of vectors we have in all the cells.
len_vecs = ave_grad.shape[0] * ave_grad.shape[1] * ave_grad.shape[2]

# Create an array that has num_bins equally spaced between 0 and 180 degress in radians.
deg = np.linspace(0, np.pi, num_bins, endpoint = False)

# Each cell will have a histogram with num_bins. For each cell, plot each bin as a vector (with its magnitude
# equal to the height of the bin in the histogram, and its angle corresponding to the bin in the histogram).
# To do this, create rank 1 arrays that will hold the (x,y)-coordinate of all the vectors in all the cells in the
# image. Also, create the rank 1 arrays that will hold all the (U,V)-components of all the vectors in all the
# cells in the image. Create the arrays that will hold all the vector positons and components.
U = np.zeros((len_vecs))
V = np.zeros((len_vecs))
X = np.zeros((len_vecs))
Y = np.zeros((len_vecs))

# Set the counter to zero
counter = 0

# Use the cosine and sine functions to calculate the vector components (U,V) from their maginitudes. Remember the
# cosine and sine functions take angles in radians. Calculate the vector positions and magnitudes from the
# average gradient array
for i in range(ave_grad.shape[0]):
    for j in range(ave_grad.shape[1]):
        for k in range(ave_grad.shape[2]):
            U[counter] = ave_grad[i,j,k] * np.cos(deg[k])
            V[counter] = ave_grad[i,j,k] * np.sin(deg[k])

            X[counter] = (cell_size[0] / 2) + (cell_size[0] * i)
            Y[counter] = (cell_size[1] / 2) + (cell_size[1] * j)

            counter = counter + 1

# Create the bins in degress to plot our histogram.
angle_axis = np.linspace(0, 180, num_bins, endpoint = False)
angle_axis += ((angle_axis[1] - angle_axis[0]) / 2)

# Create a figure with 4 subplots arranged in 2 x 2
fig, ((a,b),(c,d)) = plt.subplots(2,2)

# Set the title of each subplot
a.set(title = 'Gray Scale Image\n(Click to Zoom)')
b.set(title = 'HOG Descriptor\n(Click to Zoom)')
c.set(title = 'Zoom Window', xlim = (0, 18), ylim = (0, 18), autoscale_on = False)
d.set(title = 'Histogram of Gradients')

# Plot the gray scale image
a.imshow(gray_image, cmap = 'gray')
a.set_aspect(aspect = 1)

# Plot the feature vector (HOG Descriptor)
b.quiver(Y, X, U, V, color = 'white', headwidth = 0, headlength = 0, scale_units = 'inches', scale = 5)
b.invert_yaxis()
b.set_aspect(aspect = 1)
b.set_facecolor('black')

# Define function for interactive zoom
def onpress(event):

    #Unless the left mouse button is pressed do nothing
    if event.button != 1:
        return

    # Only accept clicks for subplots a and b
    if event.inaxes in [a, b]:

        # Get mouse click coordinates
        x, y = event.xdata, event.ydata

        # Select the cell closest to the mouse click coordinates
        cell_num_x = np.uint32(x / cell_size[0])
        cell_num_y = np.uint32(y / cell_size[1])

        # Set the edge coordinates of the rectangle patch
        edgex = x - (x % cell_size[0])
        edgey = y - (y % cell_size[1])

        # Create a rectangle patch that matches the the cell selected above
        rect = patches.Rectangle((edgex, edgey),
                                  cell_size[0], cell_size[1],
                                  linewidth = 1,
                                  edgecolor = 'magenta',
                                  facecolor='none')

        # A single patch can only be used in a single plot. Create copies
        # of the patch to use in the other subplots
        rect2 = copy.copy(rect)
        rect3 = copy.copy(rect)

        # Update all subplots
        a.clear()
        a.set(title = 'Gray Scale Image\n(Click to Zoom)')
        a.imshow(gray_image, cmap = 'gray')
        a.set_aspect(aspect = 1)
        a.add_patch(rect)

        b.clear()
        b.set(title = 'HOG Descriptor\n(Click to Zoom)')
        b.quiver(Y, X, U, V, color = 'white', headwidth = 0, headlength = 0, scale_units = 'inches', scale = 5)
        b.invert_yaxis()
        b.set_aspect(aspect = 1)
        b.set_facecolor('black')
        b.add_patch(rect2)

        c.clear()
        c.set(title = 'Zoom Window')
        c.quiver(Y, X, U, V, color = 'white', headwidth = 0, headlength = 0, scale_units = 'inches', scale = 1)
        c.set_xlim(edgex - cell_size[0], edgex + (2 * cell_size[0]))
        c.set_ylim(edgey - cell_size[1], edgey + (2 * cell_size[1]))
        c.invert_yaxis()
        c.set_aspect(aspect = 1)
        c.set_facecolor('black')
        c.add_patch(rect3)

        d.clear()
        d.set(title = 'Histogram of Gradients')
        d.grid()
        d.set_xlim(0, 180)
        d.set_xticks(angle_axis)
        d.set_xlabel('Angle')
        d.bar(angle_axis,
              ave_grad[cell_num_y, cell_num_x, :],
              180 // num_bins,
              align = 'center',
              alpha = 0.5,
              linewidth = 1.2,
              edgecolor = 'k')

        fig.canvas.draw()

# Create a connection between the figure and the mouse click
fig.canvas.mpl_connect('button_press_event', onpress)
plt.show()
```

    The feature vector has shape: (57600, 1)
    The reshaped feature vector has shape: (40, 40, 2, 2, 9)
    The average gradient array has shape:  (41, 41, 9)

    <IPython.core.display.Javascript object>

# Understanding The Histograms

Let's take a look at a couple of snapshots of the above figure to see if the histograms for the selected cell make sense. Let's start looking at a cell that is inside a triangle and not near an edge:

Fig. 4. - Histograms Inside a Triangle.

In this case, since the triangle is nearly all of the same color there shouldn't be any dominant gradient in the selected cell. As we can clearly see in the Zoom Window and the histogram, this is indeed the case. We have many gradients but none of them clearly dominates over the other.

Now let’s take a look at a cell that is near a horizontal edge:

Fig. 5. - Histograms Near a Horizontal Edge.

Remember that edges are areas of an image where the intensity changes abruptly. In these cases, we will have a high intensity gradient in some particular direction. This is exactly what we see in the corresponding histogram and Zoom Window for the selected cell.  In the Zoom Window, we can see that the dominant gradient is pointing up, almost at 90 degrees, since that’s the direction in which there is a sharp change in intensity. Therefore, we should expect to see the 90-degree bin in the histogram to dominate strongly over the others. This is in fact what we see.

Now let’s take a look at a cell that is near a vertical edge:

Fig. 6. - Histograms Near a Vertical Edge.

In this case we expect the dominant gradient in the cell to be horizontal, close to 180 degrees, since that’s the direction in which there is a sharp change in intensity. Therefore, we should expect to see the 170-degree bin in the histogram to dominate strongly over the others. This is what we see in the histogram but we also see that there is another dominant gradient in the cell, namely the one in the 10-degree bin. The reason for this, is because the HOG algorithm is using unsigned gradients, which means 0 degrees and 180 degrees are considered the same. Therefore, when the histograms are being created, angles between 160 and 180 degrees, contribute proportionally to both the 10-degree bin and the 170-degree bin. This results in there being two dominant gradients in the cell near the vertical edge instead of just one.

To conclude let’s take a look at a cell that is near a diagonal edge.

Fig. 7. - Histograms Near a Diagonal Edge.

To understand what we are seeing, let’s first remember that gradients have an *x*-component, and a *y*-component, just like vectors. Therefore, the resulting orientation of a gradient is going to be given by the vector sum of its components. For this reason, on vertical edges the gradients are horizontal, because they only have an x-component, as we saw in Figure 4. While on horizontal edges the gradients are vertical, because they only have a y-component, as we saw in Figure 3. Consequently, on diagonal edges, the gradients are also going to be diagonal because both the *x* and *y* components are non-zero.  Since the diagonal edges in the image are close to 45 degrees, we should expect to see a dominant gradient orientation in the 50-degree bin. This is in fact what we see in the histogram but, just like in Figure 4., we see there are two dominant gradients instead of just one. The reason for this is that when the histograms are being created, angles that are near the boundaries of bins, contribute proportionally to the adjacent bins.  For example, a gradient with an angle of 40 degrees, is right in the middle of the 30-degree and 50-degree bin. Therefore, the magnitude of the gradient is split evenly into the 30-degree and 50-degree bin. This results in there being two dominant gradients in the cell near the diagonal edge instead of just one.

Now that you know how HOG is implemented, in the workspace you will find a notebook named *Examples*. In there, you will be able set your own paramters for the HOG descriptor for various images. Have fun!


#MCMC integration test

Here, we'll test performance of the nGP model on sample data

```julia
import NGPtools
using Distributions
using PyPlot
plt.style[:use]("ggplot")

srand(12347) # set random seed
```

    WARNING: could not import Base.help into PyCall

    Base.Random.MersenneTwister(Base.dSFMT.DSFMT_state(Int32[-27667103,1073483597,189278614,1073164175,2057630276,1072701164,1574415675,1073236915,-1738475538,1073062896  …  1573891607,1073551291,-124871223,1072938952,1051906677,1540499365,14352668,2066903520,382,32724]),[1.28909,1.41242,1.90175,1.85695,1.86696,1.11598,1.32843,1.62557,1.55014,1.54336  …  1.21696,1.80212,1.2316,1.23841,1.24068,1.64356,1.30353,1.19172,1.59853,1.01762],382,UInt32[0x0000303b])

## Load Data

```julia
data = readcsv("data/blocks_sampled.csv")
tobs = data[:, 1]
y = data[:, 2]'
Nt = length(y)
```

    128

## Define some useful variables

```julia
δ = diff(tobs)[1]
Np = 1
Nm = 3Np
```

    3

```julia
size(tobs)
```

    (128,)

```julia
figure(figsize=(10, 6))
scatter(tobs, y);
xlim(0, 1)
```

## Pick priors and initial parameter values

```julia
σU = 1e3
σA = 5.
σϵ = 2.
σμ = 4.
σα = 4.
a, b = 1., 1.
```

    (1.0,1.0)

## Algorithm

The algorithm is as follows:

1. Given the various $\sigma^2$ parameters, perform an exact draw from the posterior for the state vector
    $(U, DU, A)$.
1. Given the state vector, draw
    $$
    \sigma^2_\epsilon \sim \text{inv-Ga}\left(
    a + \frac{N_t}{2},
    b + \frac{1}{2} \sum_t (y_t - U(t))^2
    \right)
    $$
1. Sample $\sigma^2_U$ and $\sigma^2_A$:

    1. Draw a sample of the state vector, $(U^*, DU^*, A^*)$, from the approximate model.
    1. Conditioned on this approximate state vector, sample proposed variance parameters as
        $$\begin{align}
        \sigma^2 &\sim \text{inv-Ga}\left(a + \frac{N_t}{2}, b + \frac{1}{2} R^2 \right) \\
        R^2_U &= \sum_j \frac{\left[DU^*(t_{j+1}) - DU^*(t_j) - A^*(t_j)\delta_j\right]^2}{\delta_j} \\
        R^2_A &= \sum_j \left[\frac{(A^*(t_{j+1}) - A^*(t_j))^2}{\delta_j}\right]
        \end{align}$$
    1. Accept these proposals with probability $\mathrm{min}(\pi, 1)$ with
        $$
        \pi \equiv \prod_j \frac{\mathcal{N}(\theta_{j + 1} - G_j\theta_j|0, W^*_j)}
        {\mathcal{N}(\theta_{j + 1} - G_j\theta_j|0, W_j)}
        \frac{\mathcal{N}(\tilde{H}^\top (\theta^*_{j + 1} - \tilde{G}_j\theta^*_j)|0, \tilde{W}_j)}
        {\mathcal{N}(\tilde{H}^\top (\theta^*_{j + 1} - \tilde{G}_j\theta^*_j)|0, \tilde{W}^*_j)}
        $$
        (Think of this like $\frac{P(x')}{P(x)}\frac{g(x)}{g(x')}$, with $P$ the posterior and $g$ the proposal
        distribution, and recall that what we are proposing is $\sigma^2|\theta^*$).

## Useful functions

```julia
function draw_state(σϵ, σU, σA; approx=false)
    return NGPtools.sample(y, 1, Np, length(tobs), δ, σϵ, σU, σA, σμ, σα, approx=approx)[:, :, 1]
end
```

    draw_state (generic function with 1 method)

```julia
function draw_σϵ(U)
    a_eff = a + (Nt / 2)
    b_eff = b + 0.5 * sum((y - U).^2)
    return sqrt(rand(InverseGamma(a_eff, b_eff)))
end
```

    draw_σϵ (generic function with 1 method)

```julia
function calc_acceptance_prob(θ, θstar, G, Gtilde, W, Wstar, Wtilde, Wtilde_star, Htilde)
    Nt = size(θ)[end]
    v = similar(θ)
    v_star = Array(Float64, size(Htilde)[end], Nt)

    for t in 1:(Nt - 1)
        local G_t = ndims(G) < 3 ? G : slice(G, :, :, t)
        v[:, t] = θ[:, t + 1] - G_t * θ[:, t]
        v_star[:, t] = Htilde' * (θstar[:, t + 1] - G_t * θstar[:, t])
    end

    pi = 0
    for t in 1:Nt
        local Ws = ndims(Wstar) < 3 ? Wstar : Wstar[:, :, t]
        local Ww = ndims(W) < 3 ? W : W[:, :, t]
        local Wt = ndims(Wtilde) < 3 ? Wtilde : Wtilde[:, :, t]
        local Wts = ndims(Wtilde_star) < 3 ? Wtilde_star : Wtilde_star[:, :, t]
        pi += logpdf(MvNormal(full(W)), v[:, t])
        pi -= logpdf(MvNormal(full(Ww)), v[:, t])
        pi += logpdf(MvNormal(full(Wt)), v_star[:, t])
        pi -= logpdf(MvNormal(full(Wts)), v_star[:, t])
    end

    return min(exp(pi), 1)
end
```

    calc_acceptance_prob (generic function with 1 method)

```julia
function draw_sigs(θ, σϵ, σU, σA)
    Nt = size(θ)[end]

    # draw state from approximate model
    θstar = draw_state(σϵ, σU, σA, approx=true)

    Ustar = θstar[1, :][:]
    DUstar = θstar[2, :][:]
    Astar = θstar[3, :][:]

    # calculate sufficient statistics
    d = δ * ones(Nt - 1)
    R2U = sum( (diff(DUstar) - Astar[1:(end - 1)] .* d).^2 ./ d )
    R2A = sum(diff(Astar).^2 ./ d)

    # draw proposals
    a_eff = a + (Nt / 2)
    σUstar = sqrt(rand(InverseGamma(a_eff, b + R2U/2)))
    σAstar = sqrt(rand(InverseGamma(a_eff, b + R2A/2)))

    assert(σUstar > 0)
    assert(σAstar > 0)

    _, _, G, H, W, _, _ = NGPtools.assemble_matrices(Np, δ, σϵ, σU, σA, σμ, σα)
    _, _, Gtilde, Htilde, Wtilde, _, _ = NGPtools.assemble_matrices(Np, δ, σϵ, σU, σA, σμ, σα, approx=true)
    _, _, _, _, Wstar, _, _ = NGPtools.assemble_matrices(Np, δ, σϵ, σUstar, σAstar, σμ, σα)
    _, _, _, _, Wtilde_star, _, _ = NGPtools.assemble_matrices(Np, δ, σϵ, σUstar, σAstar, σμ, σα, approx=true)

    # get acceptance probability
    acc_prob = calc_acceptance_prob(θ, θstar, G, Gtilde, W, Wstar, Wtilde, Wtilde_star, Htilde)

    if rand() < acc_prob
        return σUstar, σAstar
    else
        return σU, σA
    end
end
```

    draw_sigs (generic function with 1 method)

```julia
function fit_data(Niter, burnin, σϵ, σU, σA)
    # initialize
    th = Array(Float64, Nm, Nt, Niter + burnin)
    sig = Array(Float64, 3, Niter + burnin)

    # draw samples
    for idx in 1:(Niter + burnin)
        θ = draw_state(σϵ, σU, σA)
        σϵ = draw_σϵ(θ[1, :])
        σU, σA = draw_sigs(θ, σϵ, σU, σA)

        th[:, :, idx] = θ
        sig[:, idx] = [σϵ, σU, σA]
    end

    return th[:, :, (burnin + 1):end], sig[:, (burnin + 1):end]
end
```

    fit_data (generic function with 1 method)

```julia
Niter = 1000
burn = 500
```

    500

```julia
@time th, sig = fit_data(Niter, burn, σϵ, σU, σA);
```

     19

```julia
sig[:, end]
```

    3-element Array{Float64,1}:
       1.10486
     126.278
       0.581082

```julia
figure(figsize=(10, 6))
trans = 15. / size(th, 3)

plot(tobs, squeeze(th[1, :, :], 1), color="black", alpha=trans)
scatter(tobs, y)
xlim(0, 1)
```

```julia
plot(sig[3, :]')
```

```julia

```