---
## 02. Ecuaciones Diferenciales Ordinarias I. Método de Euler para describir el Movimiento de un Cometa

Eduard Larrañaga (ealarranaga@unal.edu.co)

---

### Resumen

En este cuaderno se presenta la aplicación del método de Euler al problema del movimiento de un cometa alrededor del Sol.

---

## Un Sistema Cometa-Sol

En este problema de aplicación se describe el movimiento de un cometa alrededor del Sol. Se considerará que la masa del cometa es mucho más pequeña que la masa del Sol, $m \ll M$, lo cual esta de acuerdo con la realidad. Por ejemplo, la masa del cometa Halley es $m \approx 2.2 \times 10^{14}$ kg, la cual es una cantidad muy pequeña comparada con la masa del Sol, $M \approx 1.9 \times 10^{30}$ kg.

Esta suposición implica que el cometa se moverá mientras que el Sol permanece estático en el origen de coordenadas. De esta forma, las ecuaciones de movimiento serán,

\begin{align}
\frac{d^2 x}{dt^2} = &- \frac{GM}{(x^2 + y^2)^{3/2}} x \\
\frac{d^2 y}{dt^2} = &- \frac{GM}{(x^2 + y^2)^{3/2}} y ,
\end{align}

donde se ha restringido el problema al plano de movimiento, con coordenadas $xy$. Para resolver este problema numéricamente se re-escribe el sistema de ecuaciones en la forma

\begin{align}
\frac{d x}{dt} = &v^x \\
\frac{d y}{dt} = &v^y \\
\frac{dv^x}{dt} = &- \frac{GM}{(x^2 + y^2)^{3/2}} x \\
\frac{dv^y}{dt} = &- \frac{GM}{(x^2 + y^2)^{3/2}} y.
\end{align}

Una consideración importante tiene relación con las magnitudes de las cantidades y constantes físicas involucradas en el problema. Para el desarrollo, utilizaremos el siguiente sistema de unidades:

- Unidades de masa: Masas Solares $M_{\odot}$
- Unidades de distancia: unidades astronómicas [au]
- Unidades de tiempo: años [yr]

En este sistema de unidades, la constante gravitacional Newtoniana toma el valor $G = 4\pi^2$.

El sistema de ecuaciones diferenciales ordinarias se implementa mediante

```python
import numpy as np

# Newtonian Gravitational Constant
G = 4.*np.pi**2
M = 1.

def f(t0, q0):
    '''
    ------------------------------------------
    f(t0, q0)
    ------------------------------------------
    ODEs system for the motion of a comet
    around the Sun using cartesian coordinates
    in the orbital plane.
    ------------------------------------------
    Arguments:
    t0: initial time
    q0: numpy array with the initial condition
        data:
        q0[0] = x
        q0[1] = y
        q0[2] = dx/dt
        q0[3] = dy/dt
    ------------------------------------------
    '''
    x = q0[0]
    y = q0[1]
    vx = q0[2]
    vy = q0[3]
    f = np.zeros(4)
    f[0] = vx
    f[1] = vy
    f[2] = - G*M*x/(x**2 + y**2)**(3/2)
    f[3] = - G*M*y/(x**2 + y**2)**(3/2)
    return f

```

---
##  Condición Inicial

Escoger la condición incial puede ser un aspecto crucial en el caso del probelma gravitacional de dos cuerpos. Una forma de establecer adecuadamente esta condición es utilizando las cantidades conservadas, que en este caso serán la energía, $E$, y el momento angular total, $l$,

\begin{equation}
E = \frac{1}{2} m \left[ (v^x)^2 + (v^y)^2\right] - \frac{GMm}{r}
\end{equation}

\begin{equation}
\vec{l} = \vec{r} \times \vec{p} = m \vec{r} \times \vec{v}.
\end{equation}

Considerando que el movimeinto ocurre en el plano $x-y$ se tiene

\begin{equation}
l = x p^y - y p^x = m(x v^y - y v^x).
\end{equation}

##  Potencial Efectivo

Otra cantidad importante para tener en cuenta es el potencial efectivo, definido en la forma

\begin{equation}
V_{\text{eff}} (r) =  \frac{l^2}{2m^2r^2} - \frac{GM}{r}.
\end{equation}

```python
import matplotlib.pyplot as plt

def eff_potential(r, h, M=1.):
  return (h**2)/(2*r**2) - G/r

r = np.linspace(0.05,5.,200)
plt.figure(figsize=(10,7))
plt.plot(r,np.zeros(len(r)),'k',linewidth=0.5)
plt.plot(r, eff_potential(r,h=2.5), color='crimson', label= r'$V_{eff}(r)$')
plt.plot(r,-25*np.ones(len(r)),color='cornflowerblue',linewidth=1.5, label='E')

plt.xlabel(r'$r$')
plt.ylabel(r'Energía')
plt.ylim(-150,100)
plt.xlim(0,5)
plt.legend()
plt.show()
```

### Condiciones para Movimiento Acotado (Trayectorias Elípticas o Circulares)

De las relaciones presentadas es posible imponer algunas relaciones para obtener la condición inicial para una clase de movimiento específico. Primero, debemos recordar que las trayectorias acotadas, para un valor fijo de momento angular, $l$, deben poseer una energía negativa, i.e. $E< 0$. Esto impone la restricción

\begin{equation}
\frac{E}{m} = \frac{1}{2} \left[(v^x)^2 + (v^y)^2\right] - \frac{GM}{r} < 0
\end{equation}
\begin{equation}
\left[(v^x)^2 + (v^y)^2\right]< \frac{2GM}{r}.
\end{equation}

Segundo, la energía total debe ser mayor (o igual) que el potencial efectivo para que exista movimiento. Esto implica que

\begin{align}
\frac{E}{m} \geq &V_{\text{eff}} (r) \\
\frac{1}{2} \left[(v^x)^2 + (v^y)^2\right] - \frac{GM}{r}  \geq &\frac{l^2}{2m^2 r^2} - \frac{GM}{r}\\
\left[(v^x)^2 + (v^y)^2\right]  \geq &\frac{(x v^y - y v^x)^2}{ r^2}
\end{align}

### Escogiendo la Condición Inicial

Utilizando  $G= 4 \pi^2$ y $M = 1 M_{\odot}$, las condiciones impuestas arriba serán

\begin{equation}
    \begin{cases}
    \left[(v^x)^2 + (v^y)^2\right]< &\frac{8\pi^2}{r} \\
    \left[(v^x)^2 + (v^y)^2\right]  \geq &\frac{(x v^y - y v^x)^2}{ r^2}
    \end{cases}
\end{equation}

Ahora podemos escoger que, en $t=0$, las coordenadas del cometa sean $x = 1 \text{ au}$  y $y=0 \text{ au}$. Con ello se obtiene

\begin{equation}
    \begin{cases}
    (v^x)^2 + (v^y)^2&<  8\pi^2 \\
    (v^x)^2 &\geq  0.
    \end{cases}
\end{equation}

El signo de igualdad en la segunda ecuación corresponde a la orbita circular (si se está localizado en el mínimo del potencial efectivo) o en la ubicación de los puntos de retorno (ápsides). Por lo tanto, asumiendo que la condición inicial corresponderá a uno de los ápsis, se toma  $v^x = 0$ y tendremos solo una ecuación que liga la otra componente de la velocidad,

\begin{equation}
    v^y < 2\sqrt{2}\pi \approx  8.885766 \frac{\text{au}}{\text{yr}}.
\end{equation}

Resumiendo estos resultados, para obtener una orbita acotada para el movimiento del cometa, se impondrá la condición incial

\begin{equation}
    \begin{cases}
    x_0 = & 1\text{ au}\\
    y_0 = & 0 \\
    v^x = & 0 \\
    v^y = & 3\frac{\text{au}}{\text{yr}}.
    \end{cases}
\end{equation}

La malla temporal que se utilizará para aplicar el método de integración se definirá desde $t_0 = 0$ hasta $t_f = 20$ años.

## Solución del Sistema de ODEs

A continuación se utilizará el método de Euler para resolver el problema diferencial.

```python
def FEuler(f, h, t0, q0):
    '''
    ------------------------------------------
    FEuler(f, h, t0, h0)
    ------------------------------------------
    Forward Euler's method for solving a ODEs
    system.
    ------------------------------------------
    Arguments:
    f : Function with the RHS of the ODEs
    h : stepsize for the iteration
    t0: initial time
    q0: NumPy array with the initial values of
        the functions in the ODEs system
    ------------------------------------------
    '''
    q1 = q0 + h*f(t0, q0)
    return q1

# Creation of the time grid (in years)
t_0 = 0.
t_f = 20.

# Number of steps in the grid
n = 100000

# Constant stepsize defined by the number of steps in the grid
dt = (t_f - t_0)/n

# Arrays to store the solution
t = np.linspace(t_0, t_f, n) # Time information
QE = np.zeros([n,4]) # Euler's Method information

# Initial Conditions
QE[0,0] = 1.
QE[0,1] = 0.
QE[0,2] = 0.
QE[0,3] = 3.

# Main loops for solving the problem
for i in range(1,n):
    q0 = QE[i-1]
    QE[i] = FEuler(f, dt, 0, q0)
```

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

fig, ax = plt.subplots(figsize=(10,7))
ax.plot(QE[:,0], QE[:,1], color='crimson', label=f'$dt =$ {dt:.2e}')
ax.set_title('Método de Euler')
ax.set_xlabel(r'$x$')
ax.set_ylabel(r'$y$')
ax.legend()

plt.show()
```

DE la trayectoria obtnida es claro que el método no esta dando un buen resultado para los parámetros inciales establecidos. Para mejorar el comportamiento, reduciremos el valor del tamaño del paso de integración,

```python
# Creation of the time grid (in years)
t_0 = 0.
t_f = 5.

# Number of steps in the grid (1 millon points!)
nI = 1000000

# Constant stepsize defined by the number of steps in the grid
dtI = (t_f - t_0)/nI

# Arrays to store the solution
tI = np.linspace(t_0, t_f, nI) # Time information
QEI = np.zeros([nI,4]) # Euler's Method information

# Initial Conditions
QEI[0,0] = 1.
QEI[0,1] = 0.
QEI[0,2] = 0.
QEI[0,3] = 3.

# Main loops for solving the problem
for i in range(1,nI):
    q0 = QEI[i-1]
    QEI[i] = FEuler(f, dtI, 0, q0)

fig, ax = plt.subplots(figsize=(10,7))
ax.plot(QEI[:,0], QEI[:,1], color='crimson', label=f'$dt = $ {dtI:.5e}')
ax.set_title('Método de Euler')
ax.set_xlabel(r'$x$')
ax.set_ylabel(r'$y$')
ax.legend()

plt.show()
```

Aún cuando el comportamiento no es el mejor, si se nota una mejoría comparado con el resultado anterior. Disminuyendo aún más el tamaño del paso se mejora la orbita calculada.

```python
# Creation of the time grid (in years)
t_0 = 0.
t_f = 5.

# Number of steps in the grid (1 millon points!)
nII = 10000000

# Constant stepsize defined by the number of steps in the grid
dtII = (t_f - t_0)/nII

# Arrays to store the solution
tII = np.linspace(t_0, t_f, nII) # Time information
QEII = np.zeros([nII,4]) # Euler's Method information

# Initial Conditions
QEII[0,0] = 1.
QEII[0,1] = 0.
QEII[0,2] = 0.
QEII[0,3] = 3.

# Main loops for solving the problem
for i in range(1,nII):
    q0 = QEII[i-1]
    QEII[i] = FEuler(f, dtII, 0, q0)

fig, ax = plt.subplots(figsize=(10,7))
ax.plot(QEII[:,0], QEII[:,1], color='crimson', label=f'$dt = $ {dtII:.5e}')
ax.set_title('Forward Euler Method')
ax.set_xlabel(r'$x$')
ax.set_ylabel(r'$y$')
ax.legend()

plt.show()
```

---

Para analizár mejor el resultado, podemos calcular la energía y el momento angular del sistema para el intervalo de tiempo integrado,

```python
EnergyE = np.zeros(n)
AngMomE = np.zeros(n)

EnergyEII = np.zeros(nII)
AngMomEII = np.zeros(nII)

for i in range(n):
    speed2 = QE[i,2]**2 + QE[i,3]**2
    r = np.sqrt(QE[i,0]**2 + QE[i,1]**2)
    EnergyE[i] = speed2/2 - G*M/r
    AngMomE[i] = QE[i,0]*QE[i,3] - QE[i,1]*QE[i,2]

for i in range(nII):
    speed2 = QEII[i,2]**2 + QEII[i,3]**2
    r = np.sqrt(QEII[i,0]**2 + QEII[i,1]**2)
    EnergyEII[i] = speed2/2 - G*M/r
    AngMomEII[i] = QEII[i,0]*QEII[i,3] - QEII[i,1]*QEII[i,2]

fig, ax = plt.subplots(2,2, figsize=(14,10))
ax[0,0].plot(QE[:,0], QE[:,1], color='crimson', label=f'$dt=$ {dt:.2e}')
ax[0,0].set_title('Método de Euler')
ax[0,0].set_xlabel(r'$x$')
ax[0,0].set_ylabel(r'$y$')
ax[0,0].legend()

ax[0,1].plot(QEII[:,0], QEII[:,1], color='cornflowerblue', label=f'$dt=$ {dtII:.2e}')
ax[0,1].set_title('Método de Euler')
ax[0,1].set_xlabel(r'$x$')
ax[0,1].set_ylabel(r'$y$')
ax[0,1].legend()

ax[1,0].plot(t, EnergyE, color='mediumslateblue', label=f'Energy')
ax[1,0].plot(t, AngMomE, color='steelblue', label=f'Angular Momentum')
ax[1,0].set_title('Método de Euler')
ax[1,0].set_xlabel(r'$t$')
ax[1,0].set_ylabel(r'$E, \ell$')
ax[1,0].legend()

ax[1,1].plot(tII, EnergyEII, color='mediumslateblue', label=f'Energy')
ax[1,1].plot(tII, AngMomEII, color='steelblue', label=f'Angular Momentum')
ax[1,1].set_title('Método de Euler')
ax[1,1].set_xlabel(r'$t$')
ax[1,1].set_ylabel(r'$E, \ell$')
ax[1,1].legend()

plt.show()
```

These plots show clearly that the forward Euler's method with a not-so-small setpsize does not satisfies the conservation of energy (and produces a small increase in the angular momentum), while the RK4 do have a very good behavior.

```python
print(f'El cambio en la energía utilizandoun paso de dt = {dt:.2e} :', np.abs(EnergyE[n-1] - EnergyE[0]))
print(f'\nEl cambio en la energía utilizandoun paso de dt = {dtII:.2e} :', np.abs(EnergyEII[n-1] - EnergyEII[0]))
```

    El cambio en la energía utilizandoun paso de dt = 2.00e-04 : 34.01224429334252

    El cambio en la energía utilizandoun paso de dt = 5.00e-07 : 2.4442463711693563e-05

```python

```

06. ODEs I. Euler y RK/02. Movimiento de un Cometa.ipynb


```python
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
plt.style.use('bmh')
import linearsolve as ls
%matplotlib inline
```

# New-Keynesian Model

This program verifies the math underlying the interactive simulation of the new-Keynesian model: https://www.briancjenkins.com/simulations/nk.html

## Equilibrium Conditions

The equilibrium conditions of the new-Keynesian model:

\begin{align}
y_{t} & = E_ty_{t+1} - \frac{1}{\sigma} r_t + g_t\\
\pi_t & = \beta E_t\pi_{t+1} + \kappa y_t + u_t\\
i_t & = \phi_{\pi} \pi_t + \phi_y y_t + v_t\\
r_t & = i_t - E_t \pi_{t+1}\\
g_{t+1} & = \rho_g g_{t} + \epsilon^g_{t+1}\\
u_{t+1} & = \rho_u u_{t} + \epsilon^u_{t+1}\\
v_{t+1} & = \rho_v v_{t} + \epsilon^v_{t+1}
\end{align}

## Analytic Solution

The model's endogenous variables as functions of exogenous state variables:

\begin{align}
y_{t} & = a_1g_t + a_2u_t + a_3v_t\\
\pi_{t} & = b_1g_t + b_2u_t + b_3v_t\\
i_{t} & = c_1g_t + c_2u_t + c_3v_t\\
r_{t} & = d_1g_t + d_2u_t + d_3v_t
\end{align}

where:

\begin{align}
a_1 & = \frac{1-\beta\rho_g}{(1-\beta\rho_g)(1-\rho_g+\sigma^{-1}\phi_y)+ \sigma^{-1}\kappa(\phi_{\pi}-\rho_g)}\\
a_2 & = -\frac{\sigma^{-1}(\phi_{\pi} - \rho_u)}{(1-\beta\rho_u)(1-\rho_u+\sigma^{-1}\phi_y)+ \sigma^{-1}\kappa(\phi_{\pi}-\rho_u)}\\
a_3 & = -\frac{\sigma^{-1}(1-\beta\rho_v)}{(1-\beta\rho_v)(1-\rho_v+\sigma^{-1}\phi_y)+ \sigma^{-1}\kappa(\phi_{\pi}-\rho_v)}
\end{align}

and:

\begin{align}
b_1 & = \frac{\kappa}{(1-\beta\rho_g)(1-\rho_g+\sigma^{-1}\phi_y)+ \sigma^{-1}\kappa(\phi_{\pi}-\rho_g)}\\
b_2 & = \frac{1-\rho_u+\sigma^{-1}\phi_y}{(1-\beta\rho_u)(1-\rho_u+\sigma^{-1}\phi_y)+ \sigma^{-1}\kappa(\phi_{\pi}-\rho_u)}\\
b_3 & = -\frac{\sigma^{-1}\kappa}{(1-\beta\rho_v)(1-\rho_v+\sigma^{-1}\phi_y)+ \sigma^{-1}\kappa(\phi_{\pi}-\rho_v)}\\
\end{align}

and:

\begin{align}
c_1 & = \phi_ya_1 + \phi_{\pi}b_1\\
c_2 & = \phi_ya_2 + \phi_{\pi}b_2\\
c_3 & = 1+ \phi_ya_3 + \phi_{\pi}b_3\\
\end{align}

and:

\begin{align}
d_1 & = c_1 - \rho_g b_1\\
d_2 & = c_2 - \rho_u b_2\\
d_3 & = c_3 - \rho_v b_3\\
\end{align}

## Compute Solution with `linearsolve`

```python
# Input model parameters
beta = np.exp(-2/100)
sigma= 1
kappa= 0.25

phi_pi= 1.5
phi_y = 0.5

rho_g = 0.25
rho_u = 0.35
rho_v = 0.5

parameters=pd.Series()
parameters.beta=beta
parameters.sigma=sigma
parameters.kappa=kappa
parameters.phi_pi=phi_pi
parameters.phi_y=phi_y
parameters.rho_g=rho_g
parameters.rho_u=rho_u
parameters.rho_v=rho_v

def equilibrium_equations(variables_forward,variables_current,parameters):

    # Parameters
    p = parameters

    # Variables
    fwd = variables_forward
    cur = variables_current

    # Euler equation
    euler_eqn = fwd.y -1/p.sigma*cur.r + cur.g - cur.y

    # NK Phillips curve
    phillips_curve = p.beta*fwd.pi + p.kappa*cur.y + cur.u - cur.pi

    # Interest rate rule for monetary policy
    interest_rule = p.phi_y*cur.y+p.phi_pi*cur.pi + cur.v - cur.i

    # Fisher equation
    fisher_eqn = cur.i - fwd.pi - cur.r

    # Exogenous demand
    g_proc =  p.rho_g*cur.g - fwd.g

    # Exogenous inflation
    u_proc =  p.rho_u*cur.u - fwd.u

    # Exogenous monetary policy
    v_proc =  p.rho_v*cur.v - fwd.v

    # Stack equilibrium conditions into a numpy array
    return np.array([
            euler_eqn,
            phillips_curve,
            interest_rule,
            fisher_eqn,
            g_proc,
            u_proc,
            v_proc
        ])

# Initialize the nk
nk = ls.model(equilibrium_equations,
              nstates=3,
              varNames=['g','u','v','i','r','y','pi'],
              parameters=parameters)

# Set the steady state of the nk
nk.set_ss([0,0,0,0,0,0,0])

# Find the log-linear approximation around the non-stochastic steady state
nk.approximate_and_solve(loglinear=False)

# Solve the nk
nk.solve_klein(nk.a,nk.b)
```

## Compute Solution Directly

```python
a1=(1-beta*rho_g)/((1-beta*rho_g)*(1-rho_g+phi_y/sigma)+kappa/sigma*(phi_pi-rho_g))
a2=-(phi_pi-rho_u)/sigma/((1-beta*rho_u)*(1-rho_u+phi_y/sigma)+kappa/sigma*(phi_pi-rho_u))
a3=-(1-beta*rho_v)/sigma/((1-beta*rho_v)*(1-rho_v+phi_y/sigma)+kappa/sigma*(phi_pi-rho_v))

b1=kappa/((1-beta*rho_g)*(1-rho_g+phi_y/sigma)+kappa/sigma*(phi_pi-rho_g))
b2=(1-rho_u+phi_y/sigma)/((1-beta*rho_u)*(1-rho_u+phi_y/sigma)+kappa/sigma*(phi_pi-rho_u))
b3=-kappa/sigma/((1-beta*rho_v)*(1-rho_v+phi_y/sigma)+kappa/sigma*(phi_pi-rho_v))

c1=phi_y*a1+phi_pi*b1
c2=phi_y*a2+phi_pi*b2
c3=phi_y*a3+phi_pi*b3+1

d1=c1-rho_g*b1
d2=c2-rho_u*b2
d3=c3-rho_v*b3
```

## Compare Analytic and Numeric Solutions

```python
print('verify a1,a2, a3:')
print(a1-nk.f[2,0])
print(a2-nk.f[2,1])
print(a3-nk.f[2,2])

print('\n')
print('verify b1,b2, b3:')
print(b1-nk.f[3,0])
print(b2-nk.f[3,1])
print(b3-nk.f[3,2])

print('\n')
print('verify c1,c2, c3:')
print(c1-nk.f[0,0])
print(c2-nk.f[0,1])
print(c3-nk.f[0,2])

print('\n')
print('verify d1,d2, d3:')
print(d1-nk.f[1,0])
print(d2-nk.f[1,1])
print(d3-nk.f[1,2])
```

    verify a1,a2, a3:
    -6.661338147750939e-16
    2.220446049250313e-15
    9.992007221626409e-16

    verify b1,b2, b3:
    3.3306690738754696e-16
    -6.661338147750939e-16
    -5.551115123125783e-17

    verify c1,c2, c3:
    5.551115123125783e-16
    -1.7763568394002505e-15
    8.326672684688674e-17

    verify d1,d2, d3:
    5.551115123125783e-16
    -1.7763568394002505e-15
    -2.7755575615628914e-16

## Plot Simulations

```python
# Compute impulse responses and plot
nk.impulse(T=15,t0=1,shocks=[0.2,0.5,1])

# Create the figure and axes
fig = plt.figure(figsize=(12,12))
ax1 = fig.add_subplot(3,1,1)
ax2 = fig.add_subplot(3,1,2)
ax3 = fig.add_subplot(3,1,3)

# Plot commands
nk.irs['e_g'][['y','pi','r','i']].plot(lw='5',alpha=0.5,grid=True,title='Demand shock',ax=ax1).legend(loc='upper right',ncol=5)
nk.irs['e_u'][['y','pi','r','i']].plot(lw='5',alpha=0.5,grid=True,title='Inflation shock',ax=ax2).legend(loc='upper right',ncol=5)
nk.irs['e_v'][['y','pi','r','i']].plot(lw='5',alpha=0.5,grid=True,title='Interest rate shock',ax=ax3).legend(loc='upper right',ncol=5)

fig.tight_layout()

```

```python

```


This notebook is an attempt at trying to get the broadband directivity functions as Lasse suggested.
In Madsen & Wahlberg 2007 (Deep Sea Research), the authors describe the integration of the directivity function over the signal's frequency range as a way to get the broadband directivity function. Let's try it out here.

2021-05-21

```python
import matplotlib.pyplot as plt
import numpy as np
import sympy
from sympy import Integral, symbols, pi, lambdify
import beamshapes
from beamshapes import piston_in_infinite_baffle as pib
from beamshapes.utilities import dB
import tqdm
```

```python
%matplotlib notebook
```

```python
a,k,theta,kmin,kmax = symbols('a k theta kmin kmax')
```

This is the famous piston in an infinite baffle directivity function.

```python
pib.d_theta
```

$\displaystyle \frac{2 J_{1}\left(a k \sin{\left(\theta \right)}\right)}{a k \sin{\left(\theta \right)}}$

Let's now integrate it wrt to the k from kmin-->kmax

```python
Integral(pib.d_theta, (k,kmin,kmax))
```

$\displaystyle \int\limits_{kmin}^{kmax} \frac{2 J_{1}\left(a k \sin{\left(\theta \right)}\right)}{a k \sin{\left(\theta \right)}}\, dk$

```python
# make SymPy solve the integral
integral_solution = Integral(pib.d_theta, (k,kmin,kmax)).doit()
integral_solution
```

$\displaystyle \frac{kmax \sqrt{\sin^{2}{\left(\theta \right)}} {{}_{1}F_{2}\left(\begin{matrix} \frac{1}{2} \\ \frac{3}{2}, 2 \end{matrix}\middle| {- \frac{a^{2} kmax^{2} \sin^{2}{\left(\theta \right)}}{4}} \right)}}{\sin{\left(\theta \right)}} - \frac{kmin \sqrt{\sin^{2}{\left(\theta \right)}} {{}_{1}F_{2}\left(\begin{matrix} \frac{1}{2} \\ \frac{3}{2}, 2 \end{matrix}\middle| {- \frac{a^{2} kmin^{2} \sin^{2}{\left(\theta \right)}}{4}} \right)}}{\sin{\left(\theta \right)}}$

So, it looks like SymPy could solve this integral - which is nice! Let's convert the expression into a function and get some outputs for broadband beamshapes!

```python
broadband_directivity = lambdify([kmin,kmax,a,theta,], integral_solution, 'sympy')
```

```python
broadband_directivity(100,200,0.1,0.001)
```

$\displaystyle 200.0 {{}_{1}F_{2}\left(\begin{matrix} 0.5 \\ 1.5, 2 \end{matrix}\middle| {-9.99999666666711 \cdot 10^{-5}} \right)} - 100.0 {{}_{1}F_{2}\left(\begin{matrix} 0.5 \\ 1.5, 2 \end{matrix}\middle| {-2.49999916666678 \cdot 10^{-5}} \right)}$

```python
thetas = np.linspace(-np.pi/2, np.pi/2,50)
kmin_v, kmax_v = 50,250
av = 0.01
bbd_dtheta = np.array(np.abs([broadband_directivity(kmin_v, kmax_v, av, each).evalf()  for each in thetas]), 'float32')
db_dtheta = dB(bbd_dtheta)
db_dtheta -= np.max(db_dtheta) # normalise to on-axis
```

```python
plt.figure()
a0 = plt.subplot(111, projection='polar')
plt.plot(thetas, db_dtheta)
a0.set_theta_zero_location("N");a0.set_thetamax(90);a0.set_thetamin(-90);plt.yticks(np.arange(-12,2,2));
plt.title(f'broadband beamshape, k:{kmin_v}-{kmax_v}')
```

    <IPython.core.display.Javascript object>

    Text(0.5, 1.0, 'broadband beamshape, k:50-250')

Let's compare this to the beamshapes at between $k=50-250$

```python
onefreq_dthetas = []
kvalues = np.arange(50,300,50)
for kv in tqdm.tqdm(kvalues):
    _, dtheta_onefreq = pib.piston_in_infinite_baffle_directivity(thetas, {'k':kv, 'a':av})
    onefreq_dthetas.append(dtheta_onefreq)

```

    100%|████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:52<00:00, 10.58s/it]

```python
plt.figure()
a1 = plt.subplot(111, projection='polar')
for each, kv in zip(onefreq_dthetas,kvalues):
    a1.plot(thetas, each,label=str(kv))
a1.set_theta_zero_location("N");a1.set_thetamax(90);a1.set_thetamin(-90);plt.yticks(np.arange(-12,2,2));
plt.legend();
```

    <IPython.core.display.Javascript object>

### Extending this exercise to other models...To be done.


# Section 7.2 $\quad$ Diagonalization and Similar Matrices

>**Definition** If $A$ and $B$ are $n\times n$ matrices, we say that $B$ is **similar** to $A$ if

In many situations, we need to compute powers of a matrix $A$.

>**Theorem** Similar matrices

**Proof**

>**Definition** If a matrix $A$ is similar to a diagonal matrix, we say

**Remark:** An $n\times n$ matrix $A$ is diagonalizable if and only if

### Example 1

Are the following matrices diagonalizable?
\begin{equation*}
  A = \left[
        \begin{array}{cc}
          1 & 1 \\
          -2 & 4 \\
        \end{array}
      \right],~~~~~
  B = \left[
        \begin{array}{cc}
          1 & 1 \\
          0 & 1 \\
        \end{array}
      \right]
\end{equation*}

```python
from sympy import *

A = Matrix([[1, 1], [-2, 4]]);
B = Matrix([[1, 1], [0, 1]]);

A.is_diagonalizable(), B.is_diagonalizable()
```

    (True, False)

If $A$ is similar to a diagonal matrix $D$, then $P^{-1}AP=D$ for some nonsingular matrix $P$.

**Question:** How to construct $P$?

### Example 2

Let $A= \left[
        \begin{array}{cc}
          1 & 1 \\
          -2 & 4 \\
        \end{array}
      \right]$ (the same matrix in Example 1). Find a matrix $P$ such that $P^{-1}AP=D$.

```python
from sympy import *

A = Matrix([[1, 1], [-2, 4]]);

A.diagonalize()
```

    (Matrix([
     [1, 1],
     [1, 2]]), Matrix([
     [2, 0],
     [0, 3]]))


# MEC6514 - Masse ajoutée

Ce document est un support au cours sur la masse ajoutée. Vous y retrouverez la démarche pour obtenir la masse ajoutée d'un cylindre circulaire, à l'aide de Python, ainsi que les formules pour d'autres formes plus complexes, fournies par Naudascher et Rockwell (1994).

## Masse ajoutée d'un cylindre circulaire

Pour pouvoir déterminer la masse ajoutée d'un cylindre circulaire, on applique la théorie des écoulements potentiels, qui nous permet de simplifier les équations de Navier-Stokes. Ceci amène les hypothèses suivantes :
- Fluide parfait ($R_e \rightarrow \infty$ et viscosité négligeable)
- Ecoulement irrotationnel (vorticité nulle)
- Fluide incompressible (hypothèse que l'on ajoute et non obligatoire)

Avant de décrire le cylindre, on importe les librairies de Python nécessaires :

```python
# librairie et outils pour le calcul symbolique
from sympy import symbols,dsolve,diff,Function,lambdify
import sympy as sp

# librairie pour le calcul numérique
import numpy as np

# librairie pour les figures et tracés
import matplotlib.pyplot as plt
```

On considère un cylindre circulaire de rayon $a$ se déplaçant à la vitesse $U$ et l'accélération $\dot{U}$ dans un fluide au repos remplissant un domaine infini (de masse volumique $\rho$).

```python
t = symbols('t') # permet de définir t comme une variable inconnue

# les paramètres suivants peuvent être changés à votre guise
a = 0.02         # rayon du cylindre (en m)
U = t+2          # Vitesse dépendant du temps, pour une accélération non nulle
rho = 1000       # masse volumique du fluide (en kg/m^3)
```

On définit les coordonnées comme cylindriques (ou polaires). Pour obtenir la masse ajoutée du cylindre, il faut déterminer dans un premier temps le champs de vitesse du fluide environnant. Pour cela, on définit les conditions limites :
- Condition de non-pénétration sur la paroi du cylindre :
    à $r=a$, la vitesse radiale $u_r=U\cos{\theta}$
- Loin du cylindre, pas d'effet de celui-ci sur l'écoulement :
    à $r=\infty$, la vitesse radiale $u_r=0$ et la vitesse azimutale $u_{\theta}=0$

On résout l'équation de Laplace en coordonnées cylindriques (et 2D), dont la solution est le potentiel de déplacement fluide $\phi(r,\theta)$ :
$\frac{1}{r} \frac{\partial}{\partial r}\left(r \frac{\partial\phi}{\partial r}\right) + \frac{1}{r^2} \frac{\partial^2\phi}{\partial\theta^2}= 0$
Pour résoudre cette équation, on applique la méthode de séparation des variables : $\phi(r,\theta) = R(r)T(\theta)$, ce qui nous donne :
$\frac{r}{R(r)}\frac{\partial}{\partial r}\left(r \frac{\partial R}{\partial r}\right)=-\frac{1}{T(\theta)} \frac{\partial^2 T}{\partial\theta^2}$
Les termes $R(r)$ et $T(\theta)$ étant indépendants, chaque côté de l'expression est égal à une constante. En posant cette constante égale à $1$, on obtient deux équations découplées, qu'il nous est possible de résoudre avec Python :
$\frac{\partial^2 T}{\partial\theta^2}+T(\theta)=0$ et $r^2 \frac{\partial^2 R}{\partial r^2} +r \frac{\partial R}{\partial r} - R(r) = 0$

```python
theta = symbols('theta')
T = Function('T')(theta) # on introduit T la fonction symbolique d'argument theta

# dsolve résoud des EDO et diff est l'opérateur différentiel symbolique
T_theta = dsolve(diff(diff(T,theta),theta)+T,T).args[1]
print('T(theta) = ',T_theta)
```

    T(theta) =  C1*sin(theta) + C2*cos(theta)

On procède de la même manière avec $R(r)$ :

```python
r = symbols('r')
R = Function('R')(r)

R_r = dsolve(r**2*diff(diff(R,r),r)+r*diff(R,r)-R,R).args[1]
print(' R(r) = ',R_r)
```

     R(r) =  C1/r + C2*r

On applique ensuite les conditions limites pour déterminer les constantes inconnues :
- $r = \infty$, $u_r=0$ $\rightarrow$ $C_2^r=0$
- $r = \infty$, $u_{\theta}=0$ $\rightarrow$ $C_2^{\theta}=C_1^{\theta}\tan{\theta}$
- $r=a$, $u_r=U\cos{\theta}$ $\rightarrow$ $2 C_1^r C_1^{\theta} = -a^2 U$

Soit :
$\phi=-\frac{a^2}{r} U\cos{\theta}$
Et :
$u_r = \frac{\partial\phi}{\partial r}=\frac{a^2}{r^2} U\cos{\theta}$, $u_{\theta} = \frac{1}{r}\frac{\partial\phi}{\partial\theta}=\frac{a^2}{r^2} U\sin{\theta}$

On peut maintenant tracer les lignes de courant autour du cylindre. Ce dernier étant en accélération, il est nécessaire de choisir un instant auquel on effectue le tracé. On a arbitrairement choisi l'instant initial $t=0$, mais vous pouvez changer celui-ci.

```python
U_0 = lambdify(t,U)(0) # changement d'instant à effectuer ici
phi = -a**2/r*U_0*sp.cos(theta)

# vitesse radiale
ur = lambdify((r,theta),diff(phi,r)) # passage d'une expression symbolique à une fonction numérique

# vitesse tangentielle (ou azimutale)
utheta = lambdify((r,theta),1/r*diff(phi,theta))

''' Calcul du champs de vitesse en coordonnées cartésiennes pour tracer '''
Ux = np.zeros((int(5*a*1000)+1,int(5*a*1000)+1))
Vy = np.zeros((int(5*a*1000)+1,int(5*a*1000)+1))

# x1 -> y et x2 -> x
x1,x2 = np.arange(-2.5*a,2.5*a+0.001,0.001), np.arange(-2.5*a,2.5*a+0.001,0.001)
i = 0
while i < int(5*a*1000)+1:
    j = 0
    while j < int(5*a*1000)+1:
        # calcul des coordonnées cylindriques
        r = np.sqrt((x1[i])**2+(x2[j])**2)
        theta = np.arcsin(x1[i]/np.sqrt((x1[i])**2+(x2[j])**2))
        if r>a:
            if x1[i] > 0 and x2[j] > 0:
                # quadrant supérieur droit
                Ux[i,j] = ur(r,theta)*np.cos(theta)-utheta(r,theta)*np.sin(theta)
                Vy[i,j] = ur(r,theta)*np.sin(theta)+utheta(r,theta)*np.cos(theta)
            elif x1[i] > 0 and x2[j] < 0:
                # quadrant supérieur gauche
                Ux[i,j] = -ur(r,np.pi-theta)*np.cos(theta)-utheta(r,np.pi-theta)*np.sin(theta)
                Vy[i,j] = ur(r,np.pi-theta)*np.sin(theta)-utheta(r,np.pi-theta)*np.cos(theta)
            elif x1[i] < 0 and x2[j] < 0:
                # quadrant inférieur gauche
                Ux[i,j] = -ur(r,np.pi-theta)*np.cos(theta)-utheta(r,np.pi-theta)*np.sin(theta)
                Vy[i,j] = ur(r,np.pi-theta)*np.sin(theta)-utheta(r,np.pi-theta)*np.cos(theta)
            elif x1[i] < 0 and x2[j] > 0:
                # quadrant inférieur droit
                Ux[i,j] = ur(r,theta)*np.cos(theta)-utheta(r,theta)*np.sin(theta)
                Vy[i,j] = ur(r,theta)*np.sin(theta)+utheta(r,theta)*np.cos(theta)
        j += 1
    i += 1

''' Tracé des lignes de courant '''
X1,X2 = np.meshgrid(x1,x2)
s_p = [(a*np.cos(i),a*np.sin(i)) for i in np.linspace(0,2*np.pi,36+1)] # sélection des points de départ des lignes de courant
cylinder = plt.Circle((0, 0),a,ec='k',fc='w',lw=3,zorder=10) # définition du cylindre

fig = plt.figure(figsize=(10,10))
ax = fig.gca()
ax.streamplot(X1,X2,Ux,Vy,color='k',density=1.5,start_points=s_p)
ax.add_patch(cylinder)
ax.set_xlim(-2.5*a,2.5*a)
ax.set_ylim(-2.5*a,2.5*a)
ax.set_xlabel('$x (m)$')
ax.set_ylabel('$y (m)$')
ax.set_title('Vélocité - Lignes de courant')
plt.show()
```

Le potentiel de déplacement fluide $\phi$ étant déterminé, on évalue la distribution de la pression $p$ avec l'équation d'Euler :
$\rho\frac{\partial\phi}{\partial t} + \rho\frac{1}{2}\left(\overrightarrow{\nabla}\phi\right)^2+p=0$

```python
# on rédéfinit ces variables car elles ont été utilisées précédemment
a,r,theta,rho,U,U_point = symbols('a r theta rho U U_point')
phi = -a**2/r*U*sp.cos(theta)
dphi_dt = -a**2/r*U_point*sp.cos(theta)

grad_phi = np.array([[diff(phi,r)],[1/r*diff(phi,theta)]]) # définition du gradient de phi
p = sp.simplify(-(rho*dphi_dt+rho*1/2*np.dot(grad_phi.T,grad_phi)))
p = p[0,0]
print('p = ',p)
```

    p =  -U**2*a**4*rho/(2*r**4) + U_point*a**2*rho*cos(theta)/r

On calcule la force exercée par le fluide sur le cylindre en intégrant la pression sur la surface du cylindre :
$F_x = \int\limits^{2\pi}_0 pa\cos{\theta}d\theta$, en $r=a$

```python
Fx = sp.integrate(p*a*sp.cos(theta),(theta,0,2*sp.pi))
Fx = sp.nsimplify(Fx,tolerance=1e-10).subs(r,a)
print('Fx = ',Fx)
```

    Fx =  pi*U_point*a**2*rho

Ce qui nous permet de déterminer la masse ajoutée du cylindre par unité de longueur $m_f$ avec $F_x=m_f\dot{U}$.

```python
mf = Fx/U_point
print('Expression de mf : ',mf)

mf = mf.subs([(sp.pi,np.pi),(a,0.02),(rho,1000)]) # on remplace les variables par leur valeur, vous pouvez les modifier
print('mf = ',mf)
```

    Expression de mf :  pi*a**2*rho
    mf =  1.25663706143592

$A_{xx}$ (respectivement $A_{yy}$) est la contribution du mouvement du cylindre dans la direction $x$ (respectivement $y$) à la masse ajoutée dans la direction $x$ (respectivement $y$). Les deux sont égales pour le cylindre circulaire et c'est leur valeur que l'on vient de calculer.
Pour le cas du cylindre circulaire, la contribution du mouvement dans la direction $y$ (respectivement $x$) à la
masse ajoutée dans la direction $x$ (respectivement $y$) $A_{xy}$ est nul.

Il nous est possible de calculer ces termes pour d'autres formes, dont les expressions ont été évaluées par Naudascher et Rockwell (1994).

Les formes suivantes sont centrées en $(0,0)$, c'est pour cela que chaque paramètre demandé est la moitié de la longueur habituellement utilisée. De plus, pour certaines formes, les ratios entre paramètres sont limités, car nous utilisons les résultats (parfois expérimentaux) fournis par Naudascher et Rockwell (1994). Vous êtes libres de modifier les paramètres pour calculer la masse ajoutée associée.

## Masse ajoutée d'une ellipse

```python
a = 0.02   # demi grand-axe (en m)
b = 0.01   # demi petit-axe (en m)
rho = 1000 # masse volumique du fluide (en kg/m3)

print('Axx = ',rho*np.pi*b**2)
print('Ayy = ',rho*np.pi*a**2)
print('Axy = ',rho*np.pi/8*(a**2-b**2)**2)
```

    Axx =  0.3141592653589793
    Ayy =  1.2566370614359172
    Axy =  3.534291735288518e-05

## Masse ajoutée d'un rectangle

```python
a = 0.02   # demie largeur (en m)
a_b = 0.2  # ratio de la demie largeur sur la demie hauteur (en m) - à choisir parmi 0.1,0.2,0.5,1.0,2.0,5.0
rho = 1000 # masse volumique du fluide (en kg/m3)

b = a/a_b
if a_b == 0.1:
    alpha = 2.23
    alpha2 = 0.147
elif a_b == 0.2:
    alpha = 1.98
    alpha2 = 0.15
elif a_b == 0.5:
    alpha = 1.7
    alpha2 = 0.15
elif a_b == 1:
    alpha = 1.51
    alpha2 = 0.234
elif a_b == 2:
    alpha = 1.36
    alpha1 = 0.15
    alpha2 = 0
elif a_b == 5:
    alpha = 1.21
    alpha1 = 0.15
    alpha2 = 0

print('Ayy = ',alpha*rho*np.pi*a**2)
if alpha2 != 0:
    print('Axy = ',alpha2*rho*np.pi*a**4)
else:
    print('Axy = ',alpha1*rho*np.pi*b**4)
```

    Ayy =  2.4881413816431164
    Axy =  7.539822368615503e-05

## Masse ajoutée d'une plaque

L'épaisseur de la plaque est considérée négligeable devant sa demie largeur.

```python
a = 0.02   # demie largeur de la plaque (en m)
rho = 1000 # masse volumique du fluide (en kg/m3)

print('Axx = 0')
print('Ayy = ',rho*np.pi*a**2)
print('Axy = ',rho*np.pi/8*a**4)
```

    Axx = 0
    Ayy =  1.2566370614359172
    Axy =  6.283185307179586e-05

## Masse ajoutée d'un losange

```python
a = 0.02   # demie largeur (en m)
a_b = 0.2  # ratio de la demie largeur sur la demie hauteur (en m) - à choisir parmi 0.2,0.5,1.0,2.0
rho = 1000 # masse volumique du fluide (en kg/m3)

b = a/a_b
if a_b == 0.2:
    alpha = 0.61
elif a_b == 0.5:
    alpha = 0.67
elif a_b == 1:
    alpha = 0.76
elif a_b == 2.0:
    alpha = 0.85

print('Ayy = ',alpha*rho*np.pi*a**2)
```

    Ayy =  0.7665486074759096

## Masse ajoutée d'une poutre en I

```python
a = 0.02   # demie largeur de la poutre (en m)
rho = 1000 # masse volumique du fluide (en kg/m3)

# tiré des expériences de Naudascher et Rockwell
d = a/2.6
b = d/3.6

print('Ayy = ',2.11*rho*np.pi*a**2)
```

    Ayy =  2.6515041996297852

## Masse ajoutée d'une poutre en croix

```python
a = 0.02   # taille d'une branche (en m)
rho = 1000 # masse volumique du fluide (en kg/m3)

print('Axx = ', rho*np.pi*a**2)
print('Axy = ',rho*2/np.pi*a**2)
```

    Axx =  1.2566370614359172
    Axy =  0.25464790894703254


# L2 Error using numerical integration

### Arnold D. Kim
*Department of Applied Mathematics, University of California, Merced*

#### Nicholas St. Clair
*Department of Physics, Kleckner Lab, University of California, Merced*

```python
import  time
print( 'Last updated: %s' %time.strftime('%d/%m/%Y') )
```

    Last updated: 30/07/2020

```python
# defaults for the codes below

import numpy as np
import scipy.special as sp
import matplotlib.pyplot as plt
```

## 1 Problem description

We compute the solution of the scattering problem for the scalar potential, $\phi_{1}$:
\begin{gather*}
   \varDelta \phi_{1} + k^{2} \phi_{1} = 0, \quad \text{in $r > a$},\\
   \partial_{r} \phi_{1} = 0 \quad \text{on $r = a$}.
\end{gather*}
The incident field is given by counter-propagating plane waves,
\begin{equation}
   \phi_{1}^{\text{inc}} = e^{\mathrm{i} k z} + e^{-\mathrm{i} k ( z + \delta )},
\end{equation}
with $\delta$ denoting a parameter that sets the location of the nodes of the standing wave. This parameter is used instead of moving the sphere so that the coordinate system can be fixed at the sphere's center.

We solve this scattering problem using two methods. The first uses the analytical representation as an expansion in Legendre polynomials. The second method uses the method of fundamental solutions (MFS).

In previous work, this scattering problem has been analytically solved for small particles $(ka << 1)$ by approximating the scattered field as a sum of monopole and dipole terms (see Eq. (17) of H. Bruus, "Acoustofluidics 7: The acoustic radiation force on small particles," Lab Chip **12**, 1014 -- 1021 (2012))
\begin{equation}
   \phi_{sc}(r,\theta) = \phi_{mp}(r) + \phi_{dp}(r,\theta), \quad \text{for $r << \lambda$}
\end{equation}
With these terms given by:
\begin{equation}
   \phi_{mp}(r) = - (1 - \frac{\kappa_{p}}{\kappa_{0}}) \frac{a^{3}}{3\rho_{0}} \partial_{t}\rho_{in} \frac{1}{r}
\end{equation}

\begin{equation}
   \phi_{dp}(r,\theta) = \frac{2(\rho_{p} - \rho_{0})}{2\rho_{p} + \rho_{0}} \frac{a^{3}}{2} v_{in} \frac{\cos(\theta)}{r^{2}}
\end{equation}
where  $\rho_{in}$ and $v_{in}$ correspond to the incident pressure and velocity fields, subscripts $p$ and $0$ denote acoustic parameters of the particles and fluid medium respectively, and $\kappa$ is the familiar compressibility factor $\frac{1}{\rho c^{2}}$.

In what follows we compare the $L^{2}$ norm of the difference between the scalar potentials from the Legendre polynomial expansion method and the monopole + dipole approximation for small particles, with the aim of demonstrating a necessity for the inclusion of higher order terms as the particle size becomes appreciable to the wavelength $(ka \approx 1)$.

## 2 Harmonic expansion for the scattered scalar potential

Here we compute the solution of the scattering problem as a harmonic expansion. This solution method is explained below.

### 2.1 Harmonic expansion for counter-propagating plane waves

We consider the following harmonic expansion for a plane wave (see Jackson (10.45)),
\begin{align}
   e^{\mathrm{i} k z} &= \sum_{n = 0}^{\infty} (\mathrm{i})^{n} \sqrt{4 \pi (2 n + 1)} j_{n}(k r)
   Y_{n0}(\theta,\varphi) \\
   &= \sum_{n = 0}^{\infty} e^{\mathrm{i} n \pi/2} ( 2 n + 1 ) j_{n}(k r)
   P_{n}(\cos\theta).
\end{align}
It follows that counter-propagating plane waves has the following harmonic expansion,
\begin{equation}
   e^{\mathrm{i} k z} + e^{-\mathrm{i} k ( z + \delta )} =
   \sum_{n = 0}^{\infty} \left( e^{\mathrm{i} n \pi/2} + e^{-\mathrm{i} n \pi/2} e^{-\mathrm{i} k \delta} \right)
   ( 2 n + 1 ) j_{n}(k r) P_{n}(\cos\theta).
\end{equation}
Using this expansion, we find that
\begin{equation}
   \partial_{r} \left( e^{\mathrm{i} k z} + e^{-\mathrm{i} k ( z + \delta )} \right) =
   \sum_{n = 0}^{\infty} \left( e^{\mathrm{i} n \pi/2} + e^{-\mathrm{i} n \pi/2} e^{-\mathrm{i} k \delta} \right)
   ( 2 n + 1 ) \partial_{r} j_{n}(k r) P_{n}(\cos\theta).
\end{equation}
Let
\begin{equation}
   B_{n} = \left( e^{\mathrm{i} n \pi/2} + e^{-\mathrm{i} n \pi/2} e^{-\mathrm{i} k \delta} \right)
   ( 2 n + 1 ).
\end{equation}
This expansion is to be used as the normal derivative of the incident field on a sound-hard sphere.

The function below computes these expansion coefficients.

```python
def ComputeIncidentHarmonicExpansionCoeffs( k, a, δ, N ):
    """
    This function computes the expansion coefficients for the field
    scattered by a sound-hard sphere due to counter-propagating plane
    waves incident on it.
    """
    # compute an array of indices from n = 0 to n = N

    n = np.arange( N )

    # compute the expansion coefficients

    B_n = ( np.exp( 1j * n * np.pi / 2.0 ) + np.exp( -1j * n * np.pi / 2.0 ) * np.exp( -1j * k * δ ) ) \
        * ( 2 * n + 1 )

    return B_n;
```

### 2.2 Harmonic expansion for the scattered field

We write the field scattered by a sphere as the harmonic expansion,
\begin{equation}
   \phi^{s} = \sum_{n = 0}^{\infty} \sum_{m = -n}^{n} c_{nm} h_{n}^{(1)}(k r) Y_{nm}(\theta,\varphi).
\end{equation}
For the counter-propagating plane waves defined above, the scattered field will be rotationally symmetric about the $z$-axis and therefore this harmonic expansion for the scattered field reduces to
\begin{equation}
   \phi^{s} = \sum_{n = 0}^{\infty} \sqrt{\frac{2n+1}{4\pi}} C_{n} h_{n}^{(1)}(k r) P_{n}(\cos\theta).
\end{equation}
It follows that
\begin{equation}
   \partial_{r} \phi^{s} = \sum_{n = 0}^{\infty} \sqrt{\frac{2n+1}{4\pi}} C_{n} \partial_{r}
   h_{n}^{(1)}(k r) P_{n}(\cos\theta).
\end{equation}
For a sound-hard sphere, $\partial_{r} ( \phi^{\text{inc}} + \phi^{s} ) = 0$ on $r = a$. Thus, we find that
\begin{equation}
   C_{n} = -\sqrt{\frac{4\pi}{2n+1}} \frac{\partial_{r} j_{n}(k a)}{\partial_{r} h_{n}^{(1)}(k a)} B_{n}.
\end{equation}
The function below computes these expansion coefficients.

```python
def ComputeScatteredHarmonicExpansionCoeffs( k, a, δ, N ):
    """
    This function computes the expansion coefficients for the field
    scattered by a sound-hard sphere due to counter-propagating plane
    waves incident on it.
    """
    # compute the expansion coefficients for the incident field

    B_n = ComputeIncidentHarmonicExpansionCoeffs( k, a, δ, N )

    # compute an array of indices from n = 0 to n = N

    n = np.arange( N )

    # compute the derivative of the spherical Bessel function

    Djn = k * sp.spherical_jn( n, k * a, derivative = True )

    # compute the derivative of the spherical Hankel function

    Dh1n = k * sp.spherical_jn( n, k * a, derivative = True ) \
        + k * 1j * sp.spherical_yn( n, k * a, derivative = True )

    # compute the expansion coefficients

    C_n = -np.sqrt( 4.0 * np.pi / ( 2 * n + 1 ) ) * Djn / Dh1n * B_n

    return C_n;
```

### 2.3 Compute Legendre polynomials

The code below computes a matrix whose $n$th column is the evaluation of the Legendre polynomial of degree $n$ on the grid points given by the vector $\mu$.

```python
def ComputeLegendrePolynomials( μ, N ):
    """
    This function computes the Gauss-Legendre quadrature rule of order N and evaluates
    the Legendre polynomials at the Gauss-Legendre quadrature points up to degree N-1.

    This function returns the Gauss-Legendre quadrature points mu, quadrature weights wt,
    and a matrix whose columns are the Legendre polynomials evaluated on mu.
    """

    # allocate memory for the matrix of Legendre polynomials

    Pn = np.full( ( μ.size, N ), 'nan', dtype = 'complex' )

    # compute the Legendre polynomial of degree 0

    Pn[:,0] = 1

    # compute the Legendre polynomial of degree 1

    Pn[:,1] = μ

    # compute the remaining Legendre polynomials using the recursion relation

    for n in range( 1, N - 1 ):

        Pn[:,n+1] = ( ( 2 * n + 1 ) * μ * Pn[:,n] - n * Pn[:,n-1] ) / ( n + 1 )

    return Pn;
```

### 2.4 Compute the derivatives of the Legendre polynomials

The code below computes the matrix whose $n$th column is the evaluation of the derivative of the Legendre polynomial of degree $n$ on the grid points given by the vector $\mu$.

```python
def ComputeLegendrePolynomialDerivatives( μ, N ):
    """
    This function computes the derivatives of the Legendre polynomials.
    """

    # allocate memory for the matrix of Legendre polynomials

    DPn = np.full( ( μ.size, N ), 'nan', dtype = 'complex' )

    # compute the derivatives of the Legendre polynomials

    DPn[:,0] = 0;
    DPn[:,1] = 1;

    # compute the current Legendre polynomial of degree n = 2

    P_nminus1 = μ
    P_n = 1.5 * μ ** 2 - 0.5

    for n in range( 2, N ):

        # compute the Legendre polynomial of degree n+1

        P_nplus1 = ( ( 2 * n + 1 ) * μ * P_n - n * P_nminus1 ) / ( n + 1 )

        # compute the derivative of the Legendre polynomial

        DPn[:,n] = ( n + 1 ) / ( μ ** 2 - 1.0 ) * ( P_nplus1 - μ * P_n )

        # update the Legendre polynomials

        P_nminus1 = P_n
        P_n       = P_nplus1

    return DPn;
```

## 3 The Method of Fundamental Solutions (MFS)

Here, we compute the solution of the scattering problem using the Method of Fundamental Solutions (MFS).

### 3.1 Compute the MFS points

We compute the Fibonnaci lattice (see A. Gonzalez, "Measurement of areas using Fibonnaci and latitude-longitude lattices," Math. Geosci. **42**, 49 (2010)) and use the resulting points on the sphere for the MFS.

```python
def ComputeMFSReferencePoints( a, N, ℓ ):
    """
    This function computes the set of points for a reference sphere that is to be used in the method of
    fundamental solutions (MFS).

    In particular, given the radius of a sphere, a, and the number of points N, this function computes
    randomly distributed points on the unit sphere and stores them as the unit normal vectors ν. Using
    ν, we then compute ρ_bdy = a * ν, and ρ_src = ( a - ℓ ) * ν.

    This function outputs three vectors: ν, ρ_bdy, and ρ_src.
    """
    # allocate memory for the Fibonacci lattice points on the unit sphere

    ν = np.full( ( N, 3 ), float( 'nan' ) )

    # compute the "golden angle"

    golden_angle = np.pi * ( 3.0 - np.sqrt( 5.0 ) )

    # compute the points on the unit sphere

    ν[:,2] = ( 1 - 1 / N ) * ( 1 - 2 * np.arange( 0, N ) / ( N - 1 ) )

    ρ = np.sqrt( 1.0 - ν[:,2] ** 2 )
    θ = golden_angle * np.arange( 0, N )

    ν[:,0] = ρ * np.cos( θ )
    ν[:,1] = ρ * np.sin( θ )

    # compute the boundary points, interior points, and exterior points

    #ℓ = 0.4 * a

    ρ_bdy = a * ν
    ρ_src = ( a - ℓ ) * ν

    return ν, ρ_bdy, ρ_src;
```

### 3.2 Compute the fundamental solution and its normal derivative

The functions below compute Green's function in a medium with wavenumber $k$ and its normal derivative.

```python
# function to compute Green's function

def ComputeG( k, R ):
    """
    This function computes the whole space Green's function given a wavenumber k and a distance R.

    The output of this function is the evaluation of the whole space Green's function.
    """

    # compute Green's function

    G = np.exp( 1j * k * R ) / ( 4 * np.pi * R )

    return G;

# function to compute the normal derivative of Green's function

def ComputeDνG( k, R, CosΘ ):
    """
    This function computes the normal derivative of the whole space Green's function
    given a wavenumber k and distance R, and the cosine of the angle made between the
    difference vector and the unit normal.

    The output of this function is the evaluation of the normal derivative of the whole
    space Green's function.
    """

    # compute Green's function

    G = ComputeG( k, R )

    # compute the normal derivative of Green's function

    DνG = CosΘ * ( 1j * k - 1 / R ) * G

    return DνG;
```

### 3.3 Compute the MFS expansion coefficients

The function below constructs and solves the linear system for the MFS expansion coefficients. This linear system results from requiring that the sound-hard boundary condition on the sphere is satisfied exactly on the Fibonnaci lattice points.

```python
def ComputeMFSExpansionCoefficients( k, δ, ν, ρ_bdy, ρ_sca, N ):
    """
    This function solves the linear system of equations for the MFS expansion coefficients.

    This code requires a wavenumber k, the results from ComputeMFSPoints, namely ν, ρ_bdy,
    and ρ_ext in addition to the number of MFS points, N, and the number of scattering
    objects, Nobjs.

    The output from this code are the N x Nobs matrix, c, corresponding to the MFS expansion
    coefficients for the scattered field.
    """

    # compute a mesh grid of indices to compute the matrices below

    indx, jndx = np.mgrid[ 0 : N , 0 : N ]

    # allocate memory for the linear system

    b = np.full( ( N, 1 ), 'nan', dtype = 'complex' )
    A = np.full( ( N, N ), 'nan', dtype = 'complex' )

    # compute the incident field (normal derivative of two, counter-propagating plane waves)

    b = -1j * k * ν[:,2] * ( np.exp( 1j * k * ρ_bdy[:,2] ) - np.exp( - 1j * k * ( ρ_bdy[:,2] + δ ) ) )

    # compute the distance from scatter points on object q to boundary points on object p

    R = np.sqrt( ( ( ρ_bdy[indx,:] - ρ_sca[jndx,:] ) ** 2 ).sum( axis = 2 ) )

    # compute the cosine of the angle made between the normal on object p and R

    CosΘ = ( ν[indx,:] * ( ρ_bdy[indx,:] - ρ_sca[jndx,:] ) ).sum( axis = 2 ) / R

    # compute the matrix sub-block

    A = ComputeDνG( k, R, CosΘ )

    # solve the linear system

    c = np.linalg.solve( A, b )

    return c;
```

## 3.3 Compute the MFS scattered scalar potential

The function below computes $\phi_{sc}$ on the sphere according to the method of fundamental solutions.

```python
def ComputeMFSphi( k, a, δ, ρ0, κ0, ω, Nμ, N, ℓ ):
    """
    This function computes the radiation force using the MFS solution for the scattered
    scalar potential φ1.
    """

    # compute the MFS points for the reference sphere

    ν, ρ_bdy, ρ_src = ComputeMFSReferencePoints( a, N, ℓ )

    # solve the linear system of equations for the MFS expansion coefficients

    c = ComputeMFSExpansionCoefficients( k, δ, ν, ρ_bdy, ρ_src, N )

    # compute the Gauss-Legendre quadrature rule points and weights

    μ, w = np.polynomial.legendre.leggauss( Nμ )

    # evaluate the solution for a fixed azimuthal angle φ

    ϕ = np.pi / Nμ * np.arange( 2 * Nμ )

    # compute a meshgrid of the angle points

    indx, jndx = np.mgrid[ 0 : Nμ, 0 : 2 * Nμ ]

    # compute the components of the normal vector

    nx = ( np.sqrt( 1.0 - μ[indx] ** 2 ) * np.cos( ϕ[jndx] ) ).flatten()
    ny = ( np.sqrt( 1.0 - μ[indx] ** 2 ) * np.sin( ϕ[jndx] ) ).flatten()
    nz = ( μ[indx] ).flatten()

    # compute the spatial points

    X = a * nx
    Y = a * ny
    Z = a * nz

    # compute the quadrature weights

    weights = a ** 2 * ( np.pi / Nμ * w[indx] ).flatten()

    ii, jj = np.mgrid[ 0:2*Nμ*Nμ, 0:N ]

    # compute the distance from source points to the evaluation point

    R = np.sqrt( ( X[ii] - ρ_src[jj,0] ) ** 2  \
                + ( Y[ii] - ρ_src[jj,1] ) ** 2 \
                + ( Z[ii] - ρ_src[jj,2] ) ** 2 )

    # compute Green's function

    G  = np.exp( 1j * k * R ) / ( 4.0 * np.pi * R )

    # compute the scattered field

    φsc = (G @ c)

    # compute the total field

    φ1 = np.exp( 1j * k * Z ) + np.exp( -1j * k * ( Z + δ ) ) + G @ c

    return φsc;
```

## 4 Compute the L2 squared difference

In spherical coordinates, the (square of the) 2-norm of the scattered scalar potential evaluated on the surface of the sphere can be written as:

\begin{equation}
    \|\phi_{sc}\|^{2} = \int_{0}^{\pi} \int_{0}^{2\pi} \left[\phi_{sc}\right]^* \phi_{sc} \sin\theta d\theta d\varphi
\end{equation}

If we allow the potential according to the MFS solution to be written as $\Phi_{sc}$, then we may express the L2 squared of the difference between potentials according to the exact method and MFS as:

\begin{equation}
    \|\Phi_{sc} - \phi_{sc}\|^{2} = \int_{0}^{\pi} \int_{0}^{2\pi} \left(\Phi_{sc} - \phi_{sc}\right)^* \left(\Phi_{sc} -\phi_{sc}\right)\sin\theta d\theta d\varphi
\end{equation}

We will take the above measure as a metric for the error in our MFS calculation.

### 4.1 Approximate Scalar Potential $(ka << 1)$

The function below computes the L2 norm of the difference between the harmonic solution to the scattered scalar potential, and the MFS solution to the scattered scalar potential.

```python
def ComputeL2diff( k, a, δ, N, ρ0, κ0, ω, NMFS, ℓ ):
    """
    This function computes the radiation force using the harmonic expansion for the
    scattered scalar potential φ1.
    """
    # compute the Gauss-Legendre quadrature rule

    μ, w = np.polynomial.legendre.leggauss( N )

    # compute the Legendre polynomials

    P_n = ComputeLegendrePolynomials( μ, N )

    # compute the harmonic expansion coefficients

    C_n = ComputeScatteredHarmonicExpansionCoeffs( k, a, δ, N )

    # compute array of indices

    n = np.arange( N )

    # compute the spherical Hankel function on the sphere

    h1n = sp.spherical_jn( n, k * a, derivative = False ) \
        + 1j * sp.spherical_yn( n, k * a, derivative = False )

    # compute the fields on the sphere using the harmonic expansions and MFS

    φ1_s_hx = P_n @ ( np.sqrt( ( 2 * n + 1 ) / ( 4.0 * np.pi ) ) * h1n * C_n )

    φ1_s_MFS = np.mean( np.reshape( ComputeMFSphi( k, a, δ, ρ0, κ0, ω, N, NMFS, ℓ ), ( N, 2 * N ) ), 1 )

    # compute the difference between the two methods

    φ1_diff = φ1_s_hx - φ1_s_MFS

    # compute the 2-norm of the scalar potential according to the harmonic expansion

    L2_hx = np.sum( w * np.abs( φ1_s_hx ) ** 2 )

    # compute the 2-norm of the scattered scalar potential on the sphere

    L2_diff =  np.sum( ( w * np.abs( φ1_diff ) ** 2 ) )

    return L2_diff;
```

```python
def ComputeL2Exp( k, a, δ, N ):
    """
    This function computes the radiation force using the harmonic expansion for the
    scattered scalar potential φ1.
    """
    # compute the Gauss-Legendre quadrature rule

    μ, w = np.polynomial.legendre.leggauss( N )

    # compute the Legendre polynomials

    P_n = ComputeLegendrePolynomials( μ, N )

    # compute the harmonic expansion coefficients

    C_n = ComputeScatteredHarmonicExpansionCoeffs( k, a, δ, N )

    # compute array of indices

    n = np.arange( N )

    # compute the spherical Hankel function on the sphere

    h1n = sp.spherical_jn( n, k * a, derivative = False ) \
        + 1j * sp.spherical_yn( n, k * a, derivative = False )

    # compute the fields on the sphere using the harmonic expansions and MFS

    φ1_s_hx = P_n @ ( np.sqrt( ( 2 * n + 1 ) / ( 4.0 * np.pi ) ) * h1n * C_n )

    # compute the 2-norm of the scalar potential according to the harmonic expansion

    L2_hx = np.sum( w * np.abs( φ1_s_hx ) ** 2 )

    return L2_hx;
```

## 5 Compare L2 Norms

In the codes that follow, we compute the 2-norm according to the sum of expansion coefficients (weighted properly), and also according to the quadrature rule integration of the scattered scalar potential on the sphere

```python
# set the frequency of the acoustic waves (kHz)

f = 40.0

# set the wave speed in air (cm kHz)

c0 = 34.3

# set the density of the fluid (air) (g/cm^3)

ρ0 = 0.001225

# set the compressibility of air

κ0 = 1.0 / ( ρ0 * c0 ** 2 )

# compute the circular frequency

ω = 2.0 * np.pi * f

# compute the wavenumber (cm^-1)

k = ω / c0
```

```python
# This cell executes the previously defined functions to compute the error as ka grows

# set the range of ka values over which we will perform the analysis

Nka = 50
ka = np.linspace( 0.1, 10, Nka )
a = ka / k

# set the number of quadrature points to use in the MFS calculation
# *note actual number of quatrature points is 2*Nq^2

N = [16,32]

# set the number of MFS points to use

NMFS = [128, 256, 512, 1028, 2056]

# set the location of field nodes

δ = np.pi / (2 * k)

# initialize arrays to store data

L2_diff = np.full( ( len( N ), Nka, len( NMFS ), 9 ), 'nan', dtype = 'float64')

L2_hx = np.full( ( len( N ), Nka ), 'nan', dtype = 'float64')

#loop over quadrature number

for n in range(len(N)):

    #loop over ka values

    for i in range(Nka):

        al =  np.linspace(0.1, 0.9, 9)

        L2_hx[n,i] = ComputeL2Exp( k, a[i], δ, N[n] )

        #loop over point source number

        for j in range(len(NMFS)):

            # loop over point source depths

            for l in range(len(al)):

                L2_diff[n,i,j,l] = ComputeL2diff( k, a[i], δ, N[n], ρ0, κ0, ω, NMFS[j], a[i] * al[l] )
```

## 6 Plot L2 difference

The following two cells plot the error in the MFS calculation as the L2 difference between MFS solution and exact solution according to the harmonic expansion. The first cell is for quadrature number 16, while the second cell is for quadrature number 32.

The final cell in this notebook plots the L2 difference as a heatmap over various number of MFS points, and various depths at which this MFS points are located under the surface of the scattering body.

```python
plt.figure(figsize  = (8,6))

plt.semilogy(ka, np.sqrt(L2_diff[0,:,0,4]/L2_hx[0]))
plt.semilogy(ka, np.sqrt(L2_diff[0,:,1,4]/L2_hx[0]))
plt.semilogy(ka, np.sqrt(L2_diff[0,:,2,4]/L2_hx[0]))
plt.semilogy(ka, np.sqrt(L2_diff[0,:,3,4]/L2_hx[0]))
plt.semilogy(ka, np.sqrt(L2_diff[0,:,4,4]/L2_hx[0]))
plt.title(r'$L^{2}[\phi_{Expansion} - \phi_{MFS}]\ /\ L^{2}[\phi_{Expansion}]$, 16 quadrature pts')
plt.legend(['128', '256', '512', '1028', '2056'], title = '# of MFS points')
plt.xlabel('ka');
plt.grid()
#plt.savefig(f'L2_Error_{NMFS}MFS_{N}quad#')
```

```python
plt.figure(figsize  = (8,6))

plt.semilogy(ka, np.sqrt(L2_diff[1,:,0,4]/L2_hx[1]))
plt.semilogy(ka, np.sqrt(L2_diff[1,:,1,4]/L2_hx[1]))
plt.semilogy(ka, np.sqrt(L2_diff[1,:,2,4]/L2_hx[1]))
plt.semilogy(ka, np.sqrt(L2_diff[1,:,3,4]/L2_hx[1]))
plt.semilogy(ka, np.sqrt(L2_diff[1,:,4,4]/L2_hx[1]))
plt.title(r'$L^{2}[\phi_{Expansion} - \phi_{MFS}]\ /\ L^{2}[\phi_{Expansion}]$, 32 quadrature points')
plt.legend(['128', '256', '512', '1028', '2056'], title = '# of MFS points')
plt.xlabel('ka');
plt.grid()
#plt.savefig(f'L2_Error_{NMFS}MFS_{N}quad#')
```

```python
ika = 25
plt.contourf(al, NMFS, np.log10(np.sqrt(L2_diff[0,ika,:,:])))
plt.colorbar()
plt.title(f'L2 Error')
plt.xlabel('scattering point depth')
plt.ylabel('# scattering points');
```

```python

```

Jupyter Notebooks/L2 error btw Harmonic & MFS soln via gauss quadrature (2).ipynb


# Impulse response equivalence

In this notebook we explore the equivalence between the two-layer model and a two-timescale impulse response approach.

## Background

Following [Geoffroy et al., 2013, Part 2](https://journals.ametsoc.org/doi/full/10.1175/JCLI-D-12-00196.1), with notation altered to match our implementation, the two-layer model with efficacy ($\epsilon$) and state-dependent climate feedback can be written as

\begin{align}
C \frac{dT}{dt} & = F - (\lambda_0 - a T) T - \epsilon \eta (T - T_D) \\
C_D \frac{dT_D}{dt} & = \eta (T - T_D)
\end{align}

If the state-dependent feedback factor, $a$, is non-zero, the two-layer model and impulse response approaches are not equivalent. However, if $a=0$, they become the same.

Hereafter we assume $a=0$, however this assumption should not be forgotten. In the case $a=0$, the two-layer model can be written (adding an $\epsilon$ for the deep-ocean equation too for simplicity later).

\begin{align}
C \frac{dT}{dt} & = F - \lambda_0 T - \epsilon \eta (T - T_D) \\
\epsilon C_D \frac{dT_D}{dt} & = \epsilon \eta (T - T_D)
\end{align}

In matrix notation we have

\begin{align}
\frac{d\mathbf{X}}{dt} = \mathbf{A} \mathbf{X} + \mathbf{B}
\end{align}

where $\mathbf{X} = \begin{pmatrix}T \\T_D\end{pmatrix}$, $\mathbf{A} = \begin{bmatrix} - \frac{\lambda_0 + \epsilon \eta}{C} & \frac{\epsilon \eta}{C} \\ \frac{\epsilon \eta}{\epsilon C_d} & -\frac{\epsilon \eta}{\epsilon C_d} \end{bmatrix}$ and $\mathbf{B} = \begin{pmatrix} \frac{F}{C} \\ 0 \end{pmatrix}$.

As shown in [Geoffroy et al., 2013, Part 1](https://journals.ametsoc.org/doi/full/10.1175/JCLI-D-12-00195.1), $\mathbf{A}$ can be diagonalised i.e. written in the form $\mathbf{A} = \mathbf{\Phi} \mathbf{D} \mathbf{\Phi}^{-1}$, where $\mathbf{D}$ is a diagonal matrix. Applying the solution given in [Geoffroy et al., 2013, Part 1](https://journals.ametsoc.org/doi/full/10.1175/JCLI-D-12-00195.1) to our impulse response notation, we have

\begin{align}
\mathbf{D} = \begin{bmatrix} - \frac{1}{\tau_1} & 0 \\ 0 & - \frac{1}{\tau_2} \end{bmatrix}
\end{align}

and

\begin{align}
\mathbf{\Phi} = \begin{bmatrix} 1 & 1 \\ \phi_1 & \phi_2 \end{bmatrix}
\end{align}

where

\begin{align}
\tau_1 = \frac{C C_D}{2 \lambda_0 \eta} (b - \sqrt{\delta})
\end{align}

\begin{align}
\tau_2 = \frac{C C_D}{2 \lambda_0 \eta} (b + \sqrt{\delta})
\end{align}

\begin{align}
\phi_1 = \frac{C}{2 \epsilon \eta} (b^* - \sqrt{\delta})
\end{align}

\begin{align}
\phi_2 = \frac{C}{2 \epsilon \eta} (b^* + \sqrt{\delta})
\end{align}

\begin{align}
b = \frac{\lambda_0 + \epsilon \eta}{C} + \frac{\eta}{C_D}
\end{align}

\begin{align}
b^* = \frac{\lambda_0 + \epsilon \eta}{C} - \frac{\eta}{C_D}
\end{align}

\begin{align}
\delta = b^2 - 4 \frac{\lambda_0 \eta}{C C_D}
\end{align}

Given this, we can re-write the system as

\begin{align}
\frac{d\mathbf{X}}{dt} &= \mathbf{\Phi} \mathbf{D} \mathbf{\Phi}^{-1} \mathbf{X} + \mathbf{B} \\
\mathbf{\Phi}^{-1}\frac{d\mathbf{X}}{dt} &= \mathbf{D} \mathbf{\Phi}^{-1} \mathbf{X} + \mathbf{\Phi}^{-1} \mathbf{B} \\
\frac{d\mathbf{Y}}{dt} &= \mathbf{D} \mathbf{Y} + \mathbf{\Phi}^{-1} \mathbf{B} \\
\end{align}

Defining $\mathbf{Y} = \begin{pmatrix} T_1 \\ T_2 \end{pmatrix}$, we have

\begin{align}
\frac{d}{dt} \begin{pmatrix} T_1 \\ T_2 \end{pmatrix} = \begin{bmatrix} - \frac{1}{\tau_1} & 0 \\ 0 & - \frac{1}{\tau_2} \end{bmatrix} \begin{pmatrix} T_1 \\ T_2 \end{pmatrix} + \frac{1}{\phi_2 - \phi_1}\begin{bmatrix} \phi_2 & -1 \\ -\phi_1 & 1 \end{bmatrix} \begin{pmatrix} \frac{F}{C} \\ 0 \end{pmatrix}
\end{align}

or,

\begin{align}
\frac{dT_1}{dt} & = \frac{-T_1}{\tau_1} + \frac{\phi_2}{\phi_2 - \phi_1} \frac{F}{C} \\
\frac{dT_2}{dt} & = \frac{-T_2}{\tau_2} - \frac{\phi_1}{\phi_2 - \phi_1} \frac{F}{C}
\end{align}

Re-writing, we have,

\begin{align}
\frac{dT_1}{dt} & = \frac{1}{\tau_1} \left( \frac{\tau_1 \phi_2}{\phi_2 - \phi_1} \frac{F}{C} - T_1 \right) \\
\frac{dT_2}{dt} & = \frac{1}{\tau_2} \left( \frac{-\tau_2 \phi_1}{\phi_2 - \phi_1} \frac{F}{C} - T_2 \right)
\end{align}

We can compare this to the notation of [Millar et al., 2017](https://doi.org/10.5194/acp-17-7213-2017) and see that

\begin{align}
d_1 = \tau_1 \\
d_2 = \tau_2 \\
q_1 = \frac{\tau_1 \phi_2}{C(\phi_2 - \phi_1)} \\
q_2 = - \frac{\tau_2 \phi_1}{C(\phi_2 - \phi_1)} \\
\end{align}

Hence we have redemonstrated the equivalence of the two-layer model and a two-timescale impulse response model. Given the parameters of the two-layer model, we can now trivially derive the equivalent parameters of the two-timescale model. Doing the reverse is possible, but requires some more work in order to make a useable route drop out.

The first step is to follow [Geoffroy et al., 2013, Part 1](https://journals.ametsoc.org/doi/full/10.1175/JCLI-D-12-00195.1), and define two extra constants

\begin{align}
a_1 = \frac{\phi_2 \tau_1}{C(\phi_2 - \phi_1)} \lambda_0 \\
a_2 = - \frac{\phi_1 \tau_2}{C(\phi_2 - \phi_1)} \lambda_0 \\
\end{align}

These constants have the useful property that $a_1 + a_2 = 1$ (proof in Appendix A).

From above, we also see that

\begin{align}
a_1 = \lambda_0 q_1 \\
a_2 = \lambda_0 q_2 \\
\end{align}

Hence

\begin{align}
a_1 + a_2 = \lambda_0 q_1 + \lambda_0 q_2 = 1 \\
\lambda_0 = \frac{1}{q_1 + q_2}
\end{align}

Next we calculate $C$ via

\begin{align}
\frac{q_1}{d_1} + \frac{q_2}{d_2} &= \frac{\phi_2}{C(\phi_2 - \phi_1)} - \frac{\phi_1}{C(\phi_2 - \phi_1)} = \frac{1}{C} \\
C &= \frac{d_1 d_2}{q_1 d_2 + q_2 d_1}
\end{align}

We then use further relationships from Table 1 of [Geoffroy et al., 2013, Part 1](https://journals.ametsoc.org/doi/full/10.1175/JCLI-D-12-00195.1) (proof is left to the reader) to calculate the rest of the constants.

Firstly,

\begin{align}
\tau_1 a_1 + \tau_2 a_2 = \frac{C + \epsilon C_D}{\lambda_0} \\
\epsilon C_D = \lambda_0 (\tau_1 a_1 + \tau_2 a_2) - C
\end{align}

and then finally,

\begin{align}
\tau_1 a_1 + \tau_2 a_2 = \frac{C + \epsilon C_D}{\lambda_0} \\
\epsilon \eta = \frac{\epsilon C_D}{\tau_1 a_2 + \tau_2 a_1}
\end{align}

The final thing to notice here is that $C_D$, $\epsilon$ and $\eta$ are not uniquely-defined. This makes sense, as shown by [Geoffroy et al., 2013, Part 2](https://journals.ametsoc.org/doi/full/10.1175/JCLI-D-12-00196.1), the introduction of the efficacy factor does not alter the behaviour of the system (it is still the same mathematical system) and so it is impossible for simply the two-timescale temperature response to uniquely define all three of these quantities. It can only define the products $\epsilon C_D$ and $\epsilon \eta$. Hence when translating from the two-timescale model to the two-layer model with efficacy, an explicit choice for the efficacy must be made. This does not alter the temperature response but it does alter the implied ocean heat uptake of the two-timescale model.

Long story short, when deriving two-layer model parameters from a two-timescale model, one must specify the efficacy.

Given that $\mathbf{Y} = \mathbf{\Phi}^{-1} \mathbf{X}$ i.e. $\mathbf{X} = \mathbf{\Phi} \mathbf{Y}$, we can also relate the impulse response boxes to the two layers.

\begin{aligned}
 \begin{pmatrix} T \\ T_D \end{pmatrix} = \begin{bmatrix} 1 & 1 \\ \phi_1 & \phi_2 \end{bmatrix} \begin{pmatrix} T_1 \\ T_2 \end{pmatrix} \\
 \begin{pmatrix} T \\ T_D \end{pmatrix} = \begin{pmatrix} T_1 + T_2 \\ \phi_1 T_1 + \phi_2 T_2 \end{pmatrix}
\end{aligned}

Finally, the equivalent of the two-timescale and two-layer models allows us to also calculate the heat uptake of a two-timescale impulse response model. It is given by

\begin{aligned}
\text{Heat uptake} &= C \frac{dT}{dt} + C_D \frac{dT_D}{dt} \\
&= F - \lambda_0 T + (1 - \epsilon) \eta (T - T_D) \\
&= F - \lambda_0 (T_1 + T_2) + (1 - \epsilon) \eta ((1 - \phi_1) T_1 + (1 - \phi_2) T_2) \\
&= F - \lambda_0 (T_1 + T_2) - \eta (\epsilon - 1)((1 - \phi_1) T_1 + (1 - \phi_2) T_2)
\end{aligned}

## Running the code

Here we actually run the two implementations to explore their similarity.

```python
import datetime as dt

import numpy as np
import pandas as pd
import openscm_units.unit_registry as ur
from scmdata.run import ScmRun, run_append

from openscm_twolayermodel import ImpulseResponseModel, TwoLayerModel

import matplotlib.pyplot as plt
```

    /Users/znicholls/Documents/AGCEC/MCastle/openscm-twolayermodel/src/openscm_twolayermodel/base.py:10: TqdmExperimentalWarning: Using `tqdm.autonotebook.tqdm` in notebook mode. Use `tqdm.tqdm` instead to force console mode (e.g. in jupyter console)
      import tqdm.autonotebook as tqdman

First we define a scenario to run.

```python
time = np.arange(1750, 2501)
forcing = 0.05 * np.sin(time / 15 * 2 * np.pi) + 3.0 * time / time.max()

inp = ScmRun(
    data=forcing,
    index=time,
    columns={
        "scenario": "test_scenario",
        "model": "unspecified",
        "climate_model": "junk input",
        "variable": "Effective Radiative Forcing",
        "unit": "W/m^2",
        "region": "World",
    },
)
inp
```

    <scmdata.ScmRun (timeseries: 1, timepoints: 751)>
    Time:
    	Start: 1750-01-01T00:00:00
    	End: 2500-01-01T00:00:00
    Meta:
    	  climate_model        model region       scenario   unit  \
    	0    junk input  unspecified  World  test_scenario  W/m^2

    	                      variable
    	0  Effective Radiative Forcing

```python
# NBVAL_IGNORE_OUTPUT
inp.lineplot()
```

Next we run the two-layer model. In order for it to be convertible to a two-timescale model, we must turn state-dependence off (a=0).

```python
two_layer_config = {
    "du": 55 * ur("m"),
    "efficacy": 1.2 * ur("dimensionless"),
    #     "efficacy": 1.0 * ur("dimensionless"),
    "a": 0 * ur("W/m^2/delta_degC^2"),
}
```

```python
# NBVAL_IGNORE_OUTPUT
twolayer = TwoLayerModel(**two_layer_config)
res_twolayer = twolayer.run_scenarios(inp)
res_twolayer
```

    HBox(children=(HTML(value='scenarios'), FloatProgress(value=1.0, bar_style='info', layout=Layout(width='20px')…

    <scmdata.ScmRun (timeseries: 4, timepoints: 751)>
    Time:
    	Start: 1750-01-01T00:00:00
    	End: 2500-01-01T00:00:00
    Meta:
    	   a (watt / delta_degree_Celsius ** 2 / meter ** 2) climate_model  \
    	0                                                0.0     two_layer
    	1                                                0.0     two_layer
    	2                                                0.0     two_layer
    	3                                                0.0     two_layer

    	   dl (meter)  du (meter)  efficacy (dimensionless)  \
    	0        1200          55                       1.2
    	1        1200          55                       1.2
    	2        1200          55                       1.2
    	3        1200          55                       1.2

    	   eta (watt / delta_degree_Celsius / meter ** 2)  \
    	0                                             0.8
    	1                                             0.8
    	2                                             0.8
    	3                                             0.8

    	   lambda0 (watt / delta_degree_Celsius / meter ** 2)        model region  \
    	0                                           1.246667   unspecified  World
    	1                                           1.246667   unspecified  World
    	2                                           1.246667   unspecified  World
    	3                                           1.246667   unspecified  World

    	   run_idx       scenario        unit                     variable
    	0        0  test_scenario       W/m^2  Effective Radiative Forcing
    	1        0  test_scenario  delta_degC    Surface Temperature|Upper
    	2        0  test_scenario  delta_degC    Surface Temperature|Lower
    	3        0  test_scenario       W/m^2                  Heat Uptake

```python
# NBVAL_IGNORE_OUTPUT
fig = plt.figure(figsize=(16, 9))

ax = fig.add_subplot(121)
res_twolayer.filter(variable="*Temperature*").lineplot(hue="variable", ax=ax)

ax = fig.add_subplot(122)
res_twolayer.filter(variable="Heat Uptake").lineplot(hue="variable", ax=ax)
```

Next we get the parameters with which we get the equivalent impulse response model.

```python
two_timescale_paras = twolayer.get_impulse_response_parameters()
two_timescale_paras
```

    {'d1': 103454323.57029569 <Unit('joule / watt')>,
     'd2': 11181891933.114195 <Unit('joule / watt')>,
     'q1': 0.4465999986742509 <Unit('delta_degree_Celsius * meter ** 2 / watt')>,
     'q2': 0.3555390387589074 <Unit('delta_degree_Celsius * meter ** 2 / watt')>,
     'efficacy': 1.2 <Unit('dimensionless')>}

```python
# NBVAL_IGNORE_OUTPUT
impulse_response = ImpulseResponseModel(**two_timescale_paras)
res_impulse_response = impulse_response.run_scenarios(inp)
res_impulse_response
```

    HBox(children=(HTML(value='scenarios'), FloatProgress(value=1.0, bar_style='info', layout=Layout(width='20px')…

    <scmdata.ScmRun (timeseries: 5, timepoints: 751)>
    Time:
    	Start: 1750-01-01T00:00:00
    	End: 2500-01-01T00:00:00
    Meta:
    	                    climate_model  d1 (joule / watt)  d2 (joule / watt)  \
    	0  two_timescale_impulse_response       1.034543e+08       1.118189e+10
    	1  two_timescale_impulse_response       1.034543e+08       1.118189e+10
    	2  two_timescale_impulse_response       1.034543e+08       1.118189e+10
    	3  two_timescale_impulse_response       1.034543e+08       1.118189e+10
    	4  two_timescale_impulse_response       1.034543e+08       1.118189e+10

    	   efficacy (dimensionless)        model  \
    	0                       1.2  unspecified
    	1                       1.2  unspecified
    	2                       1.2  unspecified
    	3                       1.2  unspecified
    	4                       1.2  unspecified

    	   q1 (delta_degree_Celsius * meter ** 2 / watt)  \
    	0                                         0.4466
    	1                                         0.4466
    	2                                         0.4466
    	3                                         0.4466
    	4                                         0.4466

    	   q2 (delta_degree_Celsius * meter ** 2 / watt) region  run_idx  \
    	0                                       0.355539  World        0
    	1                                       0.355539  World        0
    	2                                       0.355539  World        0
    	3                                       0.355539  World        0
    	4                                       0.355539  World        0

    	        scenario        unit                     variable
    	0  test_scenario       W/m^2  Effective Radiative Forcing
    	1  test_scenario  delta_degC    Surface Temperature|Box 1
    	2  test_scenario  delta_degC    Surface Temperature|Box 2
    	3  test_scenario  delta_degC          Surface Temperature
    	4  test_scenario       W/m^2                  Heat Uptake

```python
# NBVAL_IGNORE_OUTPUT
fig = plt.figure(figsize=(16, 9))

ax = fig.add_subplot(121)
res_impulse_response.filter(variable="*Temperature*").lineplot(
    hue="variable", ax=ax
)

ax = fig.add_subplot(122)
res_impulse_response.filter(variable="Heat Uptake").lineplot(
    hue="variable", ax=ax
)
```

We can compare the two responses as well.

```python
# NBVAL_IGNORE_OUTPUT
combined = run_append([res_impulse_response, res_twolayer])
combined
```

    <scmdata.ScmRun (timeseries: 9, timepoints: 751)>
    Time:
    	Start: 1750-01-01T00:00:00
    	End: 2500-01-01T00:00:00
    Meta:
    	   a (watt / delta_degree_Celsius ** 2 / meter ** 2)  \
    	0                                                NaN
    	1                                                NaN
    	2                                                NaN
    	3                                                NaN
    	4                                                NaN
    	5                                                0.0
    	6                                                0.0
    	7                                                0.0
    	8                                                0.0

    	                    climate_model  d1 (joule / watt)  d2 (joule / watt)  \
    	0  two_timescale_impulse_response       1.034543e+08       1.118189e+10
    	1  two_timescale_impulse_response       1.034543e+08       1.118189e+10
    	2  two_timescale_impulse_response       1.034543e+08       1.118189e+10
    	3  two_timescale_impulse_response       1.034543e+08       1.118189e+10
    	4  two_timescale_impulse_response       1.034543e+08       1.118189e+10
    	5                       two_layer                NaN                NaN
    	6                       two_layer                NaN                NaN
    	7                       two_layer                NaN                NaN
    	8                       two_layer                NaN                NaN

    	   dl (meter)  du (meter)  efficacy (dimensionless)  \
    	0         NaN         NaN                       1.2
    	1         NaN         NaN                       1.2
    	2         NaN         NaN                       1.2
    	3         NaN         NaN                       1.2
    	4         NaN         NaN                       1.2
    	5      1200.0        55.0                       1.2
    	6      1200.0        55.0                       1.2
    	7      1200.0        55.0                       1.2
    	8      1200.0        55.0                       1.2

    	   eta (watt / delta_degree_Celsius / meter ** 2)  \
    	0                                             NaN
    	1                                             NaN
    	2                                             NaN
    	3                                             NaN
    	4                                             NaN
    	5                                             0.8
    	6                                             0.8
    	7                                             0.8
    	8                                             0.8

    	   lambda0 (watt / delta_degree_Celsius / meter ** 2)        model  \
    	0                                                NaN   unspecified
    	1                                                NaN   unspecified
    	2                                                NaN   unspecified
    	3                                                NaN   unspecified
    	4                                                NaN   unspecified
    	5                                           1.246667   unspecified
    	6                                           1.246667   unspecified
    	7                                           1.246667   unspecified
    	8                                           1.246667   unspecified

    	   q1 (delta_degree_Celsius * meter ** 2 / watt)  \
    	0                                         0.4466
    	1                                         0.4466
    	2                                         0.4466
    	3                                         0.4466
    	4                                         0.4466
    	5                                            NaN
    	6                                            NaN
    	7                                            NaN
    	8                                            NaN

    	   q2 (delta_degree_Celsius * meter ** 2 / watt) region  run_idx  \
    	0                                       0.355539  World        0
    	1                                       0.355539  World        0
    	2                                       0.355539  World        0
    	3                                       0.355539  World        0
    	4                                       0.355539  World        0
    	5                                            NaN  World        0
    	6                                            NaN  World        0
    	7                                            NaN  World        0
    	8                                            NaN  World        0

    	        scenario        unit                     variable
    	0  test_scenario       W/m^2  Effective Radiative Forcing
    	1  test_scenario  delta_degC    Surface Temperature|Box 1
    	2  test_scenario  delta_degC    Surface Temperature|Box 2
    	3  test_scenario  delta_degC          Surface Temperature
    	4  test_scenario       W/m^2                  Heat Uptake
    	5  test_scenario       W/m^2  Effective Radiative Forcing
    	6  test_scenario  delta_degC    Surface Temperature|Upper
    	7  test_scenario  delta_degC    Surface Temperature|Lower
    	8  test_scenario       W/m^2                  Heat Uptake

To within numerical errors they are equal.

```python
# NBVAL_IGNORE_OUTPUT
fig = plt.figure(figsize=(16, 9))

ax = fig.add_subplot(121)
combined.filter(variable="*Temperature*").lineplot(
    hue="variable", style="climate_model", alpha=0.7, linewidth=2, ax=ax
)
ax.legend(loc="upper left")

ax = fig.add_subplot(122)
combined.filter(variable="Heat Uptake").lineplot(
    hue="climate_model", style="climate_model", alpha=0.7, linewidth=2, ax=ax
)
```

## Appendix A

We begin with the definitions of the $a$ constants,

\begin{align}
a_1 = \frac{\phi_2 \tau_1}{C(\phi_2 - \phi_1)} \lambda_0 \\
a_2 = - \frac{\phi_1 \tau_2}{C(\phi_2 - \phi_1)} \lambda_0 \\
\end{align}

We then have

\begin{align}
a_1 + a_2 &= \frac{\phi_2 \tau_1}{C(\phi_2 - \phi_1)} \lambda_0 - \frac{\phi_1 \tau_2}{C(\phi_2 - \phi_1)} \lambda_0 \\
&= \frac{\lambda_0}{C(\phi_2 - \phi_1)} (\phi_2 \tau_1 - \phi_1 \tau_2)
\end{align}

Recalling the definition of the $\phi$ parameters,

\begin{align}
\phi_1 = \frac{C}{2 \epsilon \eta} (b^* - \sqrt{\delta})
\end{align}

\begin{align}
\phi_2 = \frac{C}{2 \epsilon \eta} (b^* + \sqrt{\delta})
\end{align}

We have,

\begin{align}
\phi_2 - \phi_1 = \frac{C \sqrt{\delta}}{\epsilon \eta}
\end{align}
Recalling the definition of the $\tau$ parameters,

\begin{align}
\tau_1 = \frac{C C_D}{2 \lambda_0 \eta} (b - \sqrt{\delta})
\end{align}

\begin{align}
\tau_2 = \frac{C C_D}{2 \lambda_0 \eta} (b + \sqrt{\delta})
\end{align}

We have,

\begin{align}
\phi_2 \tau_1 - \phi_1 \tau_2 &= \frac{C}{2 \epsilon \eta} (b^* + \sqrt{\delta}) \times \frac{C C_D}{2 \lambda_0 \eta} (b - \sqrt{\delta}) - \frac{C}{2 \epsilon \eta} (b^* - \sqrt{\delta}) \times \frac{C C_D}{2 \lambda_0 \eta} (b + \sqrt{\delta}) \\
&= \frac{C^2 C_d}{4 \epsilon \eta^2 \lambda_0} \left[ (b^* + \sqrt{\delta}) (b - \sqrt{\delta}) - (b^* - \sqrt{\delta}) (b + \sqrt{\delta}) \right] \\
&= \frac{C^2 C_d}{4 \epsilon \eta^2 \lambda_0} \left[ b^*b + b\sqrt{\delta} -b^*\sqrt{\delta} - \delta - bb^* + b\sqrt{\delta} - b^*\sqrt{\delta} + \delta \right] \\
&= \frac{C^2 C_d}{2 \epsilon \eta^2 \lambda_0} \left[ b\sqrt{\delta} -b^*\sqrt{\delta} \right] \\
&= \frac{C^2 C_d \sqrt{\delta}}{2 \epsilon \eta^2 \lambda_0} \left[ b -b^*\right]
\end{align}

Recalling the definition of the $b$ parameters,

\begin{align}
b = \frac{\lambda_0 + \epsilon \eta}{C} + \frac{\eta}{C_D}
\end{align}

\begin{align}
b^* = \frac{\lambda_0 + \epsilon \eta}{C} - \frac{\eta}{C_D}
\end{align}

We then have

\begin{align}
\phi_2 \tau_1 - \phi_1 \tau_2 &= \frac{C^2 C_d \sqrt{\delta}}{2 \epsilon \eta^2 \lambda_0} \left[ \frac{2\eta}{C_D} \right] \\
&= \frac{C^2 \sqrt{\delta}}{\epsilon \eta \lambda_0}
\end{align}

Putting it all back together,

\begin{align}
a_1 + a_2 &= \frac{\lambda_0}{C(\phi_2 - \phi_1)} (\phi_2 \tau_1 - \phi_1 \tau_2) \\
&= \frac{\lambda_0}{C} \frac{\epsilon \eta}{C \sqrt{\delta}} \frac{C^2 \sqrt{\delta}}{\epsilon \eta \lambda_0} \\
&= 1
\end{align}


# Finite Difference Methods for the Poisson Equation
This notebook will focus on numerically approximating a inhomogenous second order Poisson Equation.
## The Differential Equation
The general two dimensional Poisson Equation is of the form:
\begin{equation}\frac{\partial^2 u}{\partial y^2} + \frac{\partial^2 u}{\partial x^2}=f(x,y), \ \ \ (x,y) \in \Omega=(0,1)\times (0,1),\end{equation}
with boundary conditions
\begin{equation}U(x,y) = g(x,y), \ \ \  (x,y)\in\delta\Omega\text{ - boundary}. \end{equation}
## Homogenous Poisson Equation
This notebook will implement a finite difference scheme to approximate the inhomogenous form of the Poisson Equation $f(x,y)=100(x^2+y^2)$:
\begin{equation}  \frac{\partial^2 u}{\partial y^2} + \frac{\partial^2 u}{\partial x^2}=100(x^2+y^2).\end{equation}
with the Boundary Conditions:
\begin{equation} u(x,0)=\sin(2\pi x), \ \ \ \ \ 0 \leq x \leq 1, \text{ lower},\end{equation}
\begin{equation} u(x,1)=\sin(2\pi x), \ \ \ \ \ 0 \leq x \leq 1, \text{ upper},\end{equation}
\begin{equation} u(0,y)=2\sin(2\pi y), \ \ \ \ \ 0 \leq y \leq 1, \text{ left},\end{equation}
\begin{equation} u(1,y)=2\sin(2\pi y), \ \ \ \ \ 0 \leq y \leq 1, \text{ right}.\end{equation}

```python
# LIBRARY
# vector manipulation
import numpy as np
# math functions
import math

# THIS IS FOR PLOTTING
%matplotlib inline
import matplotlib.pyplot as plt # side-stepping mpl backend
import warnings
warnings.filterwarnings("ignore")
from IPython.display import HTML
from mpl_toolkits.mplot3d import axes3d
import matplotlib.pyplot as plt

```

## Discete Grid
The region $\Omega=(0,1)\times(0,1)$ is discretised into a uniform mesh $\Omega_h$. In the  $x$ and $y$ directions into $N$ steps giving a stepsize of
\begin{equation} h=\frac{1-0}{N},\end{equation}
resulting in
\begin{equation}x[i]=0+ih, \ \ \  i=0,1,...,N,\end{equation}
and
\begin{equation}x[j]=0+jh, \ \ \  j=0,1,...,N,\end{equation}
The Figure below shows the discrete grid points for $N=10$,  the known boundary conditions (green),  and the unknown values (red) of the Poisson Equation.

```python
N=10
h=1/N
x=np.arange(0,1.0001,h)
y=np.arange(0,1.0001,h)
X, Y = np.meshgrid(x, y)
fig = plt.figure()
plt.plot(x[1],y[1],'ro',label='unknown');
plt.plot(X,Y,'ro');
plt.plot(np.ones(N+1),y,'go',label='Boundary Condition');
plt.plot(np.zeros(N+1),y,'go');
plt.plot(x,np.zeros(N+1),'go');
plt.plot(x, np.ones(N+1),'go');
plt.xlim((-0.1,1.1))
plt.ylim((-0.1,1.1))
plt.xlabel('x')
plt.ylabel('y')
plt.gca().set_aspect('equal', adjustable='box')
plt.legend(loc='center left', bbox_to_anchor=(1, 0.5))
plt.title(r'Discrete Grid $\Omega_h,$ h= %s'%(h),fontsize=24,y=1.08)
plt.show();
```

## Boundary Conditions

The  discrete boundary conditions are
\begin{equation} w_{i0}=w[i,0]=\sin(2\pi x[i]), \text{ for } i=0,...,10, \text{ upper},\end{equation}

\begin{equation}  w_{iN}=w[i,N]=\sin(2\pi x[i]), \text{ for } i=0,...,10,  \text{ lower},\end{equation}

\begin{equation}  w_{0j}=w[0,j]=2\sin(2\pi y[j]), \text{ for } j=0,...,10,   \text{ left},\end{equation}
\begin{equation}  w_{Nj}=w[N,j]=2\sin(2\pi y[j]), \text{ for } i=0,...,10,\text{ right}. \end{equation}

The Figure below plots the boundary values of $w[i,j]$.

```python
w=np.zeros((N+1,N+1))

for i in range (0,N):
        w[i,0]=np.sin(2*np.pi*x[i]) #left Boundary
        w[i,N]=np.sin(2*np.pi*x[i]) #Right Boundary

for j in range (0,N):
        w[0,j]=2*np.sin(2*np.pi*y[j]) #Lower Boundary
        w[N,j]=2*np.sin(2*np.pi*y[j]) #Upper Boundary

fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
# Plot a basic wireframe.
ax.plot_wireframe(X, Y, w,color='r', rstride=10, cstride=10)
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('w')
plt.title(r'Boundary Values',fontsize=24,y=1.08)
plt.show()
```

## Numerical Method
The Poisson Equation  is discretised using
$\delta_x^2$ is the central difference approximation of the second derivative in the $x$ direction
\begin{equation}\delta_x^2=\frac{1}{h^2}(w_{i+1j}-2w_{ij}+w_{i-1j}), \end{equation}
and $\delta_y^2$ is the central difference approximation of the second derivative in the $y$ direction
\begin{equation}\delta_y^2=\frac{1}{h^2}(w_{ij+1}-2w_{ij}+w_{ij-1}). \end{equation}
The gives the Poisson Difference Equation,
\begin{equation}-(\delta_x^2w_{ij}+\delta_y^2w_{ij})=f_{ij} \ \ (x_i,y_j) \in \Omega_h, \end{equation}
\begin{equation}w_{ij}=g_{ij} \ \ (x_i,y_j) \in \partial\Omega_h, \end{equation}
where $w_ij$ is the numerical approximation of $U$ at $x_i$ and $y_j$.
Expanding the  the Poisson Difference Equation gives the five point method,
\begin{equation}-(w_{i-1j}+w_{ij-1}-4w_{ij}+w_{ij+1}+w_{i+1j})=h^2f_{ij} \end{equation}
for $i=1,...,N-1$ and $j=1,...,N-1.$

### Matrix form
This can be written as a system of $(N-1)\times(N-1)$ equations can be arranged in matrix form
\begin{equation} A\mathbf{w}=\mathbf{r},\end{equation}
where $A$ is an $(N-1)^2\times(N-1)^2$  matrix made up of the following block tridiagonal structure
\begin{equation}\left(\begin{array}{ccccccc}
T&I&0&0&.&.&.\\
I&T&I&0&0&.&.\\
.&.&.&.&.&.&.\\
.&.&.&0&I&T&I\\
.&.&.&.&0&I&T\\
\end{array}\right),
\end{equation}
where $I$ denotes an $N-1 \times N-1$ identity matrix and $T$ is the tridiagonal matrix of the form:
\begin{equation} T=\left(\begin{array}{ccccccc}
-4&1&0&0&.&.&.\\
1&-4&1&0&0&.&.\\
.&.&.&.&.&.&.\\
.&.&.&0&1&-4&1\\
.&.&.&.&0&1&-4\\
\end{array}\right).
\end{equation}
The plot below shows the matrix $A$ and its inverse $A^{-1}$ as a colourplot.

```python
N2=(N-1)*(N-1)
A=np.zeros((N2,N2))
## Diagonal
for i in range (0,N-1):
    for j in range (0,N-1):
        A[i+(N-1)*j,i+(N-1)*j]=-4

# LOWER DIAGONAL
for i in range (1,N-1):
    for j in range (0,N-1):
        A[i+(N-1)*j,i+(N-1)*j-1]=1
# UPPPER DIAGONAL
for i in range (0,N-2):
    for j in range (0,N-1):
        A[i+(N-1)*j,i+(N-1)*j+1]=1

# LOWER IDENTITY MATRIX
for i in range (0,N-1):
    for j in range (1,N-1):
        A[i+(N-1)*j,i+(N-1)*(j-1)]=1

# UPPER IDENTITY MATRIX
for i in range (0,N-1):
    for j in range (0,N-2):
        A[i+(N-1)*j,i+(N-1)*(j+1)]=1
Ainv=np.linalg.inv(A)
fig = plt.figure(figsize=(12,4));
plt.subplot(121)
plt.imshow(A,interpolation='none');
clb=plt.colorbar();
clb.set_label('Matrix elements values');
plt.title('Matrix A ',fontsize=24)
plt.subplot(122)
plt.imshow(Ainv,interpolation='none');
clb=plt.colorbar();
clb.set_label('Matrix elements values');
plt.title(r'Matrix $A^{-1}$ ',fontsize=24)

fig.tight_layout()
plt.show();
```

The vector $\mathbf{w}$ is of length $(N-1)\times(N-1)$ which made up of $N-1$ subvectors $\mathbf{w}_j$  of length $N-1$ of the form
\begin{equation}\mathbf{w}_j=\left(\begin{array}{c}
w_{1j}\\
w_{2j}\\
.\\
.\\
w_{N-2j}\\
w_{N-1j}\\
\end{array}\right).
\end{equation}
The vector $\mathbf{r}$ is of length $(N-1)\times(N-1)$ which made up of $N-1$ subvectors of the form $\mathbf{r}_j=-h^2\mathbf{f}_j-\mathbf{bx}_{j}-\mathbf{by}_j$,
where $\mathbf{bx}_j $ is the vector of left and right boundary conditions
\begin{equation}\mathbf{bx}_j =\left(\begin{array}{c}
w_{0j}\\
0\\
.\\
.\\
0\\
w_{Nj}
\end{array}\right),
\end{equation}

for $j=1,..,N-1$, where $\mathbf{by}_j$ is the vector of the lower boundary condition for $j=1$,

\begin{equation}
\mathbf{by}_{1} =\left(\begin{array}{c}
w_{10}\\
w_{20}\\
.\\
.\\
w_{N-20}\\
w_{N-10}\\
\end{array}\right),
\end{equation}
upper boundary condition for $j=N-1$

\begin{equation}
\mathbf{by}_{N-1} =\left(\begin{array}{c}
w_{1N}\\
w_{2N}\\
.\\
.\\
w_{N-2N}\\
w_{N-1N}\\
\end{array}\right),
\end{equation}
for $j=2,...,N-2$ \begin{equation}\mathbf{by}_j=0,\end{equation}
and
\begin{equation}\mathbf{f}_j =100\left(\begin{array}{c}
x_1^2+y_j^2\\
x_2^2+y_j^2\\
.\\
.\\
x_{N-2}^2+y_j^2\\
x_{N-1}^2+y_j^2
\end{array}\right)
\end{equation}
for $j=1,...,N-1$.

```python
r=np.zeros(N2)

# vector r
for i in range (0,N-1):
    for j in range (0,N-1):
        r[i+(N-1)*j]=100*h*h*(x[i+1]*x[i+1]+y[j+1]*y[j+1])
# Boundary
b_bottom_top=np.zeros(N2)
for i in range (0,N-1):
    b_bottom_top[i]=np.sin(2*np.pi*x[i+1]) #Bottom Boundary
    b_bottom_top[i+(N-1)*(N-2)]=np.sin(2*np.pi*x[i+1])# Top Boundary

b_left_right=np.zeros(N2)
for j in range (0,N-1):
    b_left_right[(N-1)*j]=2*np.sin(2*np.pi*y[j+1]) # Left Boundary
    b_left_right[N-2+(N-1)*j]=2*np.sin(2*np.pi*y[j+1])# Right Boundary

b=b_left_right+b_bottom_top
```

## Results

To solve the system for $\mathbf{w}$ invert the matrix $A$
\begin{equation} A\mathbf{w}=\mathbf{r},\end{equation}
such that
\begin{equation} \mathbf{w}=A^{-1}\mathbf{r}.\end{equation}
Lastly, as $\mathbf{w}$ is in vector it has to be reshaped into grid form to plot.

The figure below shows the numerical approximation of the homogeneous Equation.

```python
C=np.dot(Ainv,r-b)
w[1:N,1:N]=C.reshape((N-1,N-1))

fig = plt.figure(figsize=(8,6))
ax = fig.add_subplot(111, projection='3d');
# Plot a basic wireframe.
ax.plot_wireframe(X, Y, w,color='r');
ax.set_xlabel('x');
ax.set_ylabel('y');
ax.set_zlabel('w');
plt.title(r'Numerical Approximation of the Poisson Equation',fontsize=24,y=1.08);
plt.show();
```

# Consistency and Convergence
We now ask how well the grid function determined by the five point scheme approximates the exact solution of the Poisson problem.
## Consistency

### Consitency (Definition)
Let
\begin{equation}\nabla^2_h(\varphi)=-(\varphi_{i-1j}+\varphi_{ij-1}-4\varphi_{ij}+\varphi_{ij+1}+\varphi_{i+1j})\end{equation}
denote the finite difference approximation associated with the grid $\Omega_h$ having the mesh size $h$, to a partial differential operator
\begin{equation}\nabla^2(\varphi)=\frac{\partial^2 \varphi}{\partial x^2}+\frac{\partial^2 \varphi}{\partial y^2}\end{equation} defined on
a simply connected, open set $\Omega \subset R^2$. For a given function $\varphi\in C^{\infty}(\Omega)$,
the truncation error of $\nabla^2_h$ is
\begin{equation}\tau_{h}(\mathbf{x})=(\nabla^2-\nabla^2_h)\varphi(\mathbf{x}) \end{equation}
The approximation $\nabla^2_h$ is consistent with $\nabla^2$ if
\begin{equation} \lim_{h\rightarrow 0}\tau_h(\mathbf{x})=0,\end{equation}
for all $\mathbf{x} \in D$ and all $\varphi \in C^{\infty}(\Omega)$. The approximation is consistent to order $p$ if $\tau_h(\mathbf{x})=O(h^p)$.

_In other words a method is consistent with the differential equation it is approximating._

## Proof of Consistency
The five-point difference analog $\nabla^2_h$ is consistent to order 2 with $\nabla^2$.

__Proof__

Pick $\varphi \in C^{\infty}(D)$, and let $(x,y) \in \Omega$ be a point such that $(x\pm h, y),(x,y \pm h) \in \Omega\bigcup \partial\Omega$.  By the Taylor Theorem
\begin{eqnarray*}
\varphi(x\pm h,y)&=&\varphi(x,y) \pm h \frac{\partial \varphi}{\partial x}(x,y)+\frac{h^2}{2!}\frac{\partial^2 \varphi}{\partial x^2}(x,y) \pm\frac{h^3}{3!}\frac{\partial^3 \varphi}{\partial x^3}(x,y)+\frac{h^4}{4!}\frac{\partial^4 \varphi}{\partial x^4}(\zeta^{\pm},y)
\end{eqnarray*}
where $\zeta^{\pm} \in (x-h,x+h)$. Adding this pair of equation together and rearranging , we get
\begin{equation}\frac{1}{h^2}[\varphi(x+h,y)-2\varphi(x,y)+\varphi(x-h,y) ] -\frac{\partial^2 \varphi}{\partial x^2}(x,y)=\frac{h^2}{4!}\left[\frac{\partial^4 \varphi}{\partial x^4}(\zeta^{+},y)+
\frac{\partial^4 \varphi}{\partial x^4}(\zeta^{-},y)
 \right]
\end{equation}
By the intermediate value theorem
\begin{equation}\left[\frac{\partial^4 \varphi}{\partial x^4}(\zeta^{+},y)+
\frac{\partial^4 \varphi}{\partial x^4}(\zeta^{-},y)
 \right]
=2\frac{\partial^4 \varphi}{\partial x^4}(\zeta,y),\end{equation}
for some $\zeta \in (x-h,x+h)$.  Therefore,
\begin{equation}\delta_x^2(x,y)
=\frac{\partial^2 \varphi}{\partial x^2}(x,y)+\frac{h^2}{2!}\frac{\partial^4 \varphi}{\partial x^4}(\zeta,y)\end{equation}
Similar reasoning shows that
\begin{equation}\delta_y^2(x,y)
=\frac{\partial^2 \varphi}{\partial y^2}(x,y)+\frac{h^2}{2!}\frac{\partial^4 \varphi}{\partial y^4}(x,\eta)
\end{equation}
for some $\eta \in (y-h,y+h)$. We conclude that $\tau_h(x,y)=(\nabla-\nabla_h)\varphi(x,y)=O(h^2).$

## Convergence
### Definition
Let $\nabla^2_hw(\mathbf{x}_j)=f(\mathbf{x}_j)$ be a finite difference approximation, defined on a grid mesh size $h$, to a PDE $\nabla^2U(\mathbf{x})=f(\mathbf{x})$ on a simply connected set $D \subset R^n$. Assume that $w(x,y)=U(x,y)$ at all points $(x,y)$ on the boundary $\partial\Omega$.  The finite difference scheme converges (or is convergent) if
\begin{equation} \max_j|U(\mathbf{x}_j)-w(\mathbf{x}_j)| \rightarrow 0 \mbox{  as  } h \rightarrow 0.\end{equation}

### Theorem (DISCRETE MAXIMUM PRINCIPLE).
If $\nabla^2_hV_{ij}\geq 0$ for all points $(x_i,y_j) \in \Omega_h$, then
\begin{equation} \max_{(x_i,y_j)\in\Omega_h}V_{ij}\leq  \max_{(x_i,y_j)\in\partial\Omega_h}V_{ij},\end{equation}
If $\nabla^2_hV_{ij}\leq 0$ for all points $(x_i,y_j) \in \Omega_h$, then
\begin{equation} \min_{(x_i,y_j)\in\Omega_h}V_{ij}\geq  \min_{(x_i,y_j)\in\partial\Omega_h}V_{ij}.\end{equation}

### Propositions
1. The zero grid function for which $U_{ij}=0$ for all $(x_i,y_j) \in \Omega_h \bigcup \partial\Omega_h$
is the only solution to the finite difference problem
\begin{equation}\nabla_h^2U_{ij}=0 \mbox{ for }(x_i,y_j)\in\Omega_h,\end{equation}
\begin{equation}U_{ij}=0 \mbox{ for }(x_i,y_j)\in\partial\Omega_h.\end{equation}

2. For prescribed grid functions $f_{ij}$ and $g_{ij}$, there exists a unique solution to the problem
\begin{equation}\nabla_h^2U_{ij}=f_{ij} \mbox{ for }(x_i,y_j)\in\Omega_h,\end{equation}
\begin{equation}U_{ij}=g_{ij} \mbox{ for }(x_i,y_j)\in\partial\Omega_h.\end{equation}

### Definition
For any grid function $V:\Omega_h\bigcup\partial\Omega_h \rightarrow R$,
\begin{equation}||V||_{\Omega} =\max_{(x_i,y_j)\in\Omega_h}|V_{ij}|, \end{equation}
\begin{equation}||V||_{\partial\Omega} =\max_{(x_i,y_j)\in\partial\Omega_h}|V_{ij}|. \end{equation}

### Lemma
If the grid function $V:\Omega_h\bigcup\partial\Omega_h\rightarrow R$ satisfies the boundary condition $V_{ij}=0$ for $(x_i,y_j)\in \partial\Omega_h$, then
\begin{equation}||V_||_{\Omega}\leq \frac{1}{8}||\nabla_h^2V||_{\Omega}. \end{equation}

Given these Lemmas and Propositions, we can now prove that the solution to the five point scheme $\nabla^2_h$ is convergent to the exact solution of the Poisson Equation $\nabla^2$.
### Convergence Theorem
Let $U$ be a solution to the Poisson equation and let $w$ be the grid function
that satisfies the discrete analog
\begin{equation}-\nabla_h^2w_{ij}=f_{ij} \ \ \mbox{ for } (x_i,y_j)\in\Omega_h, \end{equation}
\begin{equation}w_{ij}=g_{ij} \ \ \mbox{ for } (x_i,y_j)\in\partial\Omega_h. \end{equation}
Then there exists a positive constant $K$ such that
\begin{equation}||U-w||_{\Omega}\leq KMh^2, \end{equation}
where
\begin{equation} M=\left\{
\left|\left|\frac{\partial^4 U}{\partial x^4} \right|\right|_{\infty},
\left|\left|\frac{\partial^4 U}{\partial y^4} \right|\right|_{\infty}
 \right\}\end{equation}

 __Proof__

 The statement of the theorem assumes that $U\in C^4(\bar{\Omega})$. This assumption
holds if $f$ and $g$ are smooth enough.
\begin{proof}
Following from the proof of the Proposition we have
\begin{equation} (\nabla_h^2-\nabla^2)U_{ij}=\frac{h^2}{12}\left[ \frac{\partial^4 U}{\partial x^4}(\zeta_i,y_j)+\frac{\partial^4 U}{\partial y^4}(x_i,\eta_j) \right],\end{equation}
for some $\zeta \in (x_{i-1},x_{i+1})$ and $\eta_j\in(y_{j-1},y_{j+1})$.  Therefore,
\begin{equation} -\nabla_h^2U_{ij}=f_{ij}-\frac{h^2}{12}\left[ \frac{\partial^4 U}{\partial x^4}(\zeta_i,y_j)+\frac{\partial^4 U}{\partial y^4}(x_i,\eta_j) \right].\end{equation}
If we subtract from this the identity equation $-\nabla_h^2w_{ij}=f_{ij}$ and note
that $U-w$ vanishes on $\partial\Omega_h$, we find that
\begin{equation} \nabla_h^2(U_{ij}-w_{ij})=\frac{h^2}{12}\left[ \frac{\partial^4 U}{\partial x^4}(\zeta_i,y_j)+\frac{\partial^4 U}{\partial y^4}(x_i,\eta_j) \right].\end{equation}
It follows that

\begin{equation} ||U-w||_{\Omega}\leq\frac{1}{8}||\nabla_h^2(U-w)||_{\Omega}\leq KMh^2.\end{equation}

```python

```

Chapter 09 - Elliptic Equations/903_Poisson Equation-Boundary.ipynb