# Matrix generation

## Init symbols for *sympy*

```python
from sympy import *
from geom_util import *
from sympy.vector import CoordSys3D
N = CoordSys3D('N')
alpha1, alpha2, alpha3 = symbols("alpha_1 alpha_2 alpha_3", real = True, positive=True)
init_printing()

%matplotlib inline

%reload_ext autoreload
%autoreload 2
%aimport geom_util
```

### Lame params

```python
h1 = Function("H1")
h2 = Function("H2")
h3 = Function("H3")

H1 = h1(alpha1, alpha2, alpha3)
H2 = S(1
H3 = h3(alpha1, alpha2, alpha3)
```

### Metric tensor

${\displaystyle \hat{G}=\sum_{i,j} g^{ij}\vec{R}_i\vec{R}_j}$

```python
G_up = getMetricTensorUpLame(H1, H2, H3)
```

${\displaystyle \hat{G}=\sum_{i,j} g_{ij}\vec{R}^i\vec{R}^j}$

```python
G_down = getMetricTensorDownLame(H1, H2, H3)
```

### Christoffel symbols

```python
GK = getChristoffelSymbols2(G_up, G_down, (alpha1, alpha2, alpha3))
```

### Gradient of vector

$
\left(
\begin{array}{c}
\nabla_1 u_1 \\ \nabla_2 u_1 \\ \nabla_3 u_1 \\
\nabla_1 u_2 \\ \nabla_2 u_2 \\ \nabla_3 u_2 \\
\nabla_1 u_3 \\ \nabla_2 u_3 \\ \nabla_3 u_3 \\
\end{array}
\right)
=
B \cdot
\left(
\begin{array}{c}
u_1 \\
\frac { \partial u_1 } { \partial \alpha_1} \\
\frac { \partial u_1 } { \partial \alpha_2} \\
\frac { \partial u_1 } { \partial \alpha_3} \\
u_2 \\
\frac { \partial u_2 } { \partial \alpha_1} \\
\frac { \partial u_2 } { \partial \alpha_2} \\
\frac { \partial u_2 } { \partial \alpha_3} \\
u_3 \\
\frac { \partial u_3 } { \partial \alpha_1} \\
\frac { \partial u_3 } { \partial \alpha_2} \\
\frac { \partial u_3 } { \partial \alpha_3} \\
\end{array}
\right)
= B \cdot D \cdot
\left(
\begin{array}{c}
u^1 \\
\frac { \partial u^1 } { \partial \alpha_1} \\
\frac { \partial u^1 } { \partial \alpha_2} \\
\frac { \partial u^1 } { \partial \alpha_3} \\
u^2 \\
\frac { \partial u^2 } { \partial \alpha_1} \\
\frac { \partial u^2 } { \partial \alpha_2} \\
\frac { \partial u^2 } { \partial \alpha_3} \\
u^3 \\
\frac { \partial u^3 } { \partial \alpha_1} \\
\frac { \partial u^3 } { \partial \alpha_2} \\
\frac { \partial u^3 } { \partial \alpha_3} \\
\end{array}
\right)
$

```python
def row_index_to_i_j_grad(i_row):
    return i_row // 3, i_row % 3

B = zeros(9, 12)
B[0,1] = S(1)
B[1,2] = S(1)

B[2,3] = S(1)

B[3,5] = S(1)
B[4,6] = S(1)
B[5,7] = S(1)

B[6,9] = S(1)
B[7,10] = S(1)
B[8,11] = S(1)

for row_index in range(9):
    i,j=row_index_to_i_j_grad(row_index)
    B[row_index, 0] = -GK[i,j,0]
    B[row_index, 4] = -GK[i,j,1]
    B[row_index, 8] = -GK[i,j,2]

B
```

### Strain tensor

$
\left(
\begin{array}{c}
\varepsilon_{11} \\
\varepsilon_{22} \\
\varepsilon_{33} \\
2\varepsilon_{12} \\
2\varepsilon_{13} \\
2\varepsilon_{23} \\
\end{array}
\right)
=
\left(E + E_{NL} \left( \nabla \vec{u} \right) \right) \cdot
\left(
\begin{array}{c}
\nabla_1 u_1 \\ \nabla_2 u_1 \\ \nabla_3 u_1 \\
\nabla_1 u_2 \\ \nabla_2 u_2 \\ \nabla_3 u_2 \\
\nabla_1 u_3 \\ \nabla_2 u_3 \\ \nabla_3 u_3 \\
\end{array}
\right)$

```python
E=zeros(6,9)
E[0,0]=1
E[1,4]=1
E[2,8]=1
E[3,1]=1
E[3,3]=1
E[4,2]=1
E[4,6]=1
E[5,5]=1
E[5,7]=1
E

```

```python
def E_NonLinear(grad_u):
    N = 3

    du = zeros(N, N)

    #    print("===Deformations===")

    for i in range(N):
        for j in range(N):
            index = i*N+j
            du[j,i] = grad_u[index]

    #    print("========")

    a_values = S(1)/S(2) * du * G_up

    E_NL = zeros(6,9)
    E_NL[0,0] = a_values[0,0]
    E_NL[0,3] = a_values[0,1]
    E_NL[0,6] = a_values[0,2]

    E_NL[1,1] = a_values[1,0]
    E_NL[1,4] = a_values[1,1]
    E_NL[1,7] = a_values[1,2]

    E_NL[2,2] = a_values[2,0]
    E_NL[2,5] = a_values[2,1]
    E_NL[2,8] = a_values[2,2]

    E_NL[3,1] = 2*a_values[0,0]
    E_NL[3,4] = 2*a_values[0,1]
    E_NL[3,7] = 2*a_values[0,2]

    E_NL[4,0] = 2*a_values[2,0]
    E_NL[4,3] = 2*a_values[2,1]
    E_NL[4,6] = 2*a_values[2,2]

    E_NL[5,2] = 2*a_values[1,0]
    E_NL[5,5] = 2*a_values[1,1]
    E_NL[5,8] = 2*a_values[1,2]

    return E_NL

%aimport geom_util
#u=getUHat3D(alpha1, alpha2, alpha3)

u=getUHatU3Main(alpha1, alpha2, alpha3)

gradu=B*u

E_NL = E_NonLinear(gradu)*B

```

### Physical coordinates

$u_i=u_{[i]} H_i$

```python
P=zeros(12,12)
P[0,0]=H1
P[1,0]=(H1).diff(alpha1)
P[1,1]=H1
P[2,0]=(H1).diff(alpha2)
P[2,2]=H1
P[3,0]=(H1).diff(alpha3)
P[3,3]=H1

P[4,4]=H2
P[5,4]=(H2).diff(alpha1)
P[5,5]=H2
P[6,4]=(H2).diff(alpha2)
P[6,6]=H2
P[7,4]=(H2).diff(alpha3)
P[7,7]=H2

P[8,8]=H3
P[9,8]=(H3).diff(alpha1)
P[9,9]=H3
P[10,8]=(H3).diff(alpha2)
P[10,10]=H3
P[11,8]=(H3).diff(alpha3)
P[11,11]=H3
P=simplify(P)
P
```

```python
B_P = zeros(9,9)

for i in range(3):
    for j in range(3):
        ratio=1
        if (i==0):
            ratio = ratio*H1
        elif (i==1):
            ratio = ratio*H2
        elif (i==2):
            ratio = ratio*H3

        if (j==0):
            ratio = ratio*H1
        elif (j==1):
            ratio = ratio*H2
        elif (j==2):
            ratio = ratio*H3

        row_index = i*3+j

        B_P[row_index, row_index] = 1/ratio

Grad_U_P = simplify(B_P*B*P)
B_P
```

```python
StrainL=simplify(E*Grad_U_P)
StrainL
```

```python
%aimport geom_util

u=getUHatU3Main(alpha1, alpha2, alpha3)

gradup=B_P*B*P*u

E_NLp = E_NonLinear(gradup)*B*P*u

simplify(E_NLp)
```

### Tymoshenko theory

$u^1 \left( \alpha_1, \alpha_2, \alpha_3 \right)=u\left( \alpha_1 \right)+\alpha_3\gamma \left( \alpha_1 \right) $

$u^2 \left( \alpha_1, \alpha_2, \alpha_3 \right)=0 $

$u^3 \left( \alpha_1, \alpha_2, \alpha_3 \right)=w\left( \alpha_1 \right) $

$ \left(
\begin{array}{c}
u^1 \\
\frac { \partial u^1 } { \partial \alpha_1} \\
\frac { \partial u^1 } { \partial \alpha_2} \\
\frac { \partial u^1 } { \partial \alpha_3} \\
u^2 \\
\frac { \partial u^2 } { \partial \alpha_1} \\
\frac { \partial u^2 } { \partial \alpha_2} \\
\frac { \partial u^2 } { \partial \alpha_3} \\
u^3 \\
\frac { \partial u^3 } { \partial \alpha_1} \\
\frac { \partial u^3 } { \partial \alpha_2} \\
\frac { \partial u^3 } { \partial \alpha_3} \\
\end{array}
\right) = T \cdot
\left(
\begin{array}{c}
u \\
\frac { \partial u } { \partial \alpha_1} \\
\gamma \\
\frac { \partial \gamma } { \partial \alpha_1} \\
w \\
\frac { \partial w } { \partial \alpha_1} \\
\end{array}
\right) $

```python
T=zeros(12,6)
T[0,0]=1
T[0,2]=alpha3
T[1,1]=1
T[1,3]=alpha3
T[3,2]=1

T[8,4]=1
T[9,5]=1
T
```

```python
D_p_T = StrainL*T
simplify(D_p_T)
```

```python
u = Function("u")
t = Function("theta")
w = Function("w")

u1=u(alpha1)+alpha3*t(alpha1)
u3=w(alpha1)

gu = zeros(12,1)
gu[0] = u1
gu[1] = u1.diff(alpha1)
gu[3] = u1.diff(alpha3)

gu[8] = u3
gu[9] = u3.diff(alpha1)

gradup=Grad_U_P*gu

# o20=(K*u(alpha1)-w(alpha1).diff(alpha1)+t(alpha1))/2
# o21=K*t(alpha1)
# O=1/2*o20*o20+alpha3*o20*o21-alpha3*K/2*o20*o20
# O=expand(O)
# O=collect(O,alpha3)
# simplify(O)

StrainNL = E_NonLinear(gradup)*gradup
simplify(StrainNL)
```

### Square theory

$u^1 \left( \alpha_1, \alpha_2, \alpha_3 \right)=u_{10}\left( \alpha_1 \right)p_0\left( \alpha_3 \right)+u_{11}\left( \alpha_1 \right)p_1\left( \alpha_3 \right)+u_{12}\left( \alpha_1 \right)p_2\left( \alpha_3 \right) $

$u^2 \left( \alpha_1, \alpha_2, \alpha_3 \right)=0 $

$u^3 \left( \alpha_1, \alpha_2, \alpha_3 \right)=u_{30}\left( \alpha_1 \right)p_0\left( \alpha_3 \right)+u_{31}\left( \alpha_1 \right)p_1\left( \alpha_3 \right)+u_{32}\left( \alpha_1 \right)p_2\left( \alpha_3 \right) $

$ \left(
\begin{array}{c}
u^1 \\
\frac { \partial u^1 } { \partial \alpha_1} \\
\frac { \partial u^1 } { \partial \alpha_2} \\
\frac { \partial u^1 } { \partial \alpha_3} \\
u^2 \\
\frac { \partial u^2 } { \partial \alpha_1} \\
\frac { \partial u^2 } { \partial \alpha_2} \\
\frac { \partial u^2 } { \partial \alpha_3} \\
u^3 \\
\frac { \partial u^3 } { \partial \alpha_1} \\
\frac { \partial u^3 } { \partial \alpha_2} \\
\frac { \partial u^3 } { \partial \alpha_3} \\
\end{array}
\right) = L \cdot
\left(
\begin{array}{c}
u_{10} \\
\frac { \partial u_{10} } { \partial \alpha_1} \\
u_{11} \\
\frac { \partial u_{11} } { \partial \alpha_1} \\
u_{12} \\
\frac { \partial u_{12} } { \partial \alpha_1} \\
u_{30} \\
\frac { \partial u_{30} } { \partial \alpha_1} \\
u_{31} \\
\frac { \partial u_{31} } { \partial \alpha_1} \\
u_{32} \\
\frac { \partial u_{32} } { \partial \alpha_1} \\
\end{array}
\right) $

```python
L=zeros(12,12)
h=Symbol('h')
p0=1/2-alpha3/h
p1=1/2+alpha3/h
p2=1-(2*alpha3/h)**2

L[0,0]=p0
L[0,2]=p1
L[0,4]=p2

L[1,1]=p0
L[1,3]=p1
L[1,5]=p2

L[3,0]=p0.diff(alpha3)
L[3,2]=p1.diff(alpha3)
L[3,4]=p2.diff(alpha3)

L[8,6]=p0
L[8,8]=p1
L[8,10]=p2

L[9,7]=p0
L[9,9]=p1
L[9,11]=p2

L[11,6]=p0.diff(alpha3)
L[11,8]=p1.diff(alpha3)
L[11,10]=p2.diff(alpha3)

L
```

```python
B_General = zeros(9, 12)
B_General[0,1] = S(1)
B_General[1,2] = S(1)
B_General[2,3] = S(1)

B_General[3,5] = S(1)
B_General[4,6] = S(1)
B_General[5,7] = S(1)

B_General[6,9] = S(1)
B_General[7,10] = S(1)
B_General[8,11] = S(1)

for row_index in range(9):
    i,j=row_index_to_i_j_grad(row_index)

    B_General[row_index, 0] = -Symbol("G_{{{}{}}}^1".format(i+1,j+1))
    B_General[row_index, 4] = -Symbol("G_{{{}{}}}^2".format(i+1,j+1))
    B_General[row_index, 8] = -Symbol("G_{{{}{}}}^3".format(i+1,j+1))

B_General
```

```python
simplify(B_General*L)
```

```python
simplify(E*B*D*P*L)
```

```python
D_p_L = StrainL*L
K = Symbol('K')
D_p_L = D_p_L.subs(R, 1/K)
simplify(D_p_L)
```

```python
h = 0.5
exp=(0.5-alpha3/h)*(1-(2*alpha3/h)**2)#/(1+alpha3*0.8)
p02=integrate(exp, (alpha3, -h/2, h/2))
integral = expand(simplify(p02))
integral
```

## Mass matrix

```python
rho=Symbol('rho')
B_h=zeros(3,12)
B_h[0,0]=1
B_h[1,4]=1
B_h[1,8]=1
M=simplify(rho*P.T*B_h.T*G_up*B_h*P)
M
```


# Non-linear Gaussian filtering and smoothing (continuous-discrete)

Provided is an example of a nonlinear state-space model on which one can perform Bayesian filtering and smoothing in order to obtain
a posterior distribution over a latent state trajectory based on noisy observations.
In order to understand the theory behind these methods in detail we refer to [1] and [2].

In order to perform Bayesian Filtering and Smoothing on non-linear models, we show how to use functionalities provided by ProbNum to
either linearise the respective model (Extended Kalman Filter, EKF) or to use an Unscented Transform (Unscented Kalman Filter, UKF) which does not require a Jacobian of the nonlinear model.

**References**:
> [1] Särkkä, Simo, and Solin, Arno. Applied Stochastic Differential Equations. Cambridge University Press, 2019.
>
> [2] Särkkä, Simo. Bayesian Filtering and Smoothing. Cambridge University Press, 2013.

```python
import numpy as np

import probnum as pn
from probnum import filtsmooth, randvars, randprocs
from probnum.problems import TimeSeriesRegressionProblem
```

```python
rng = np.random.default_rng(seed=123)
```

```python
# Make inline plots vector graphics instead of raster graphics
%matplotlib inline
from IPython.display import set_matplotlib_formats

set_matplotlib_formats("pdf", "svg")

# Plotting
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec

plt.style.use("../../probnum.mplstyle")
```

    /tmp/ipykernel_18296/236124620.py:5: DeprecationWarning: `set_matplotlib_formats` is deprecated since IPython 7.23, directly use `matplotlib_inline.backend_inline.set_matplotlib_formats()`
      set_matplotlib_formats("pdf", "svg")

## **Non-Linear Continuous-Discrete** State-Space Model: Benes-Daum Filter

Now, consider we assume **continuous** dynamics. We assume that there is a continuous process that defines the nonlinear dynamics of our latent space from which we collect discrete nonlinear-Gaussian measurements (as above). Only the dynamics model changes. In particular, we formulate the dynamics as a stochastic process in terms of a Nonlinear Time-Invariant Stochastic Differential Equation. We refer to [1] for more details.
Again, consider non-linear functions $f: \mathbb{R}^d \rightarrow \mathbb{R}^d$ and $h: \mathbb{R}^d \rightarrow \mathbb{R}^m$ where $d$ is the dimension of the latent state and
$m$ is the dimension of the measurements.
We define the following nonlinear **continuous-discrete** state-space model:

Let $x(t_0) \sim \mathcal{N}(\mu_0, \Sigma_0)$.

$$
\begin{align}
    d\boldsymbol{x} &= f(\boldsymbol{x}) \, dt + \boldsymbol{L} \, d \boldsymbol{\omega} \\
    \boldsymbol{y}_k &\sim \mathcal{N}(h(\boldsymbol{x}(t_k)), \boldsymbol{R}), \qquad k = 1, \dots, K
\end{align}
$$

where $\boldsymbol{\omega}$ denotes a vector of driving forces (often Brownian Motion).

Note that this can be generalized to a nonlinear time-varying state-space model, as well. Then we would have $f: \mathbb{T} \times \mathbb{R}^d \rightarrow \mathbb{R}^d$ and $h: \mathbb{T} \times \mathbb{R}^d \rightarrow \mathbb{R}^m$ where $\mathbb{T}$ is the "time dimension".

### Define State-Space Model

#### I. Continuous Dynamics Model: Nonlinear Stochastic Differential Equation

```python
state_dim = 1
observation_dim = 1
```

```python
drift_function = lambda t, state: np.tanh(state)
drift_function_jacobian = lambda t, state: 1.0 - np.tanh(state) ** 2

dispersion_function = lambda t, x: np.ones(state_dim).reshape(1, 1)
```

For continous, non-linear dynamics, `probnum` provides a general stochastic differential equation (SDE) interfaces.

```python
# Create continuous, non-linear SDE dynamics model
dynamics_model = randprocs.markov.continuous.SDE(
    state_dimension=state_dim,
    wiener_process_dimension=1,
    drift_function=drift_function,
    dispersion_function=dispersion_function,
    drift_jacobian=drift_function_jacobian,
)
```

#### II. Discrete Measurement Model: Linear, Time-Invariant, Gaussian Measurements

```python
measurement_marginal_variance = 1.0
measurement_matrix = np.eye(observation_dim, state_dim)
measurement_noise_matrix = measurement_marginal_variance * np.eye(observation_dim)
```

```python
measurement_model = randprocs.markov.discrete.LTIGaussian(
    transition_matrix=measurement_matrix,
    noise = randvars.Normal(np.zeros(observation_dim), measurement_noise_matrix),
)
```

#### III. Initial State Random Variable

```python
mu_0 = np.zeros(state_dim)
sigma_0 = 3.0 * np.eye(state_dim)
initial_state_rv = randvars.Normal(mean=mu_0, cov=sigma_0)
```

### Kalman Filtering

#### I. Linearize model (Extended Kalman Filter)

```python
linearised_dynamics_model = filtsmooth.gaussian.approx.ContinuousEKFComponent(
    dynamics_model
)
```

#### II. Kalman Filter

```python
prior_process = randprocs.markov.MarkovProcess(
    transition=linearised_dynamics_model, initrv=initial_state_rv, initarg=0.0
)
kalman_filter = filtsmooth.gaussian.Kalman(prior_process)
```

#### III. Generate Data for the State-Space Model

`generate_artificial_measurements()` is used to sample both latent states and noisy observations from the specified state space model.

```python
delta_t = 0.01
time_grid = np.arange(0.0, 10.0, step=10 * delta_t)
```

```python
latent_states, observations = randprocs.markov.utils.generate_artificial_measurements(
    rng=rng,
    prior_process=prior_process,
    measmod=measurement_model,
    times=time_grid,
)
```

```python
regression_problem = TimeSeriesRegressionProblem(
    observations=observations,
    locations=time_grid,
    measurement_models=[measurement_model] * len(time_grid),
)
```

#### IV. Perform Kalman Filtering + Rauch-Tung-Striebel Smoothing

```python
state_posterior, _ = kalman_filter.filter(regression_problem)
```

```python
smoothed_state_posterior = kalman_filter.smooth(filter_posterior=state_posterior)
```

The method `filtsmooth` returns a `KalmanPosterior` object which provides convenience functions for e.g. sampling and prediction.
We can also extract the just computed posterior smoothing state variables by querying the `.state_rvs` property.
This yields a list of Gaussian Random Variables from which we can extract the statistics in order to visualize them.

```python
grid = np.linspace(0, 11, 500)

posterior_state_rvs = state_posterior(
    grid
)  # List of <num_time_points> Normal Random Variables
posterior_state_means = (
    posterior_state_rvs.mean.squeeze()
)  # Shape: (num_time_points, state_dim)
posterior_state_covs = (
    posterior_state_rvs.cov
)  # Shape: (num_time_points, state_dim, state_dim)
```

```python
smoothed_posterior_state_rvs = (
    smoothed_state_posterior.states
)  # List of <num_time_points> Normal Random Variables
smoothed_posterior_state_means = (
    smoothed_posterior_state_rvs.mean.squeeze()
)  # Shape: (num_time_points, state_dim)
smoothed_posterior_state_covs = (
    smoothed_posterior_state_rvs.cov
)  # Shape: (num_time_points, state_dim, state_dim)
```

### Visualize Results

```python
state_fig = plt.figure()

ax = state_fig.add_subplot()

# Plot means
ax.plot(grid, posterior_state_means, label="posterior mean")

# Plot marginal standard deviations
std_x = np.sqrt(np.abs(posterior_state_covs)).squeeze()
ax.fill_between(
    grid,
    posterior_state_means - 1.96 * std_x,
    posterior_state_means + 1.96 * std_x,
    alpha=0.2,
    label="1.96 marginal stddev for filter",
)

# Plot smoothed means
ax.plot(time_grid, smoothed_posterior_state_means, label="smoothed posterior mean")

# Plot marginal standard deviations
std_x = np.sqrt(smoothed_posterior_state_covs).squeeze()
ax.fill_between(
    time_grid,
    smoothed_posterior_state_means - 1.96 * std_x,
    smoothed_posterior_state_means + 1.96 * std_x,
    alpha=0.2,
    label="1.96 marginal stddev for smoother",
)

ax.scatter(time_grid, observations, marker=".", label="measurements")

# Add labels etc.
ax.set_xlabel("t")
ax.set_title(r"$x$")
ax.legend()

state_fig.tight_layout()
```

To get a closer look at filter and smoother, we inspect the interval $[0, 1]$ in greater detail.

```python
idx = time_grid < 1.0

ax.set_xlim([0, 1])
ax.set_ylim(np.min(observations[idx]), np.max(observations[idx]))

state_fig.tight_layout()
state_fig
```


### Distribution function for the DM mini-halo

The idea is the construct an equilibrium distribution function for a DM mini-halo (around a PBH) with a density profile $\rho \sim r^{-3/2}$ within the truncation radius $r_\mathrm{tr}$.

```python
import numpy as np
import sympy as sp

from tqdm import tqdm
from scipy.interpolate import interp1d
from scipy.interpolate import UnivariateSpline
from scipy.integrate import quad
from scipy.special import betainc, beta, btdtr,hyp2f1

from sympy.assumptions import assuming, Q

from __future__ import division

from matplotlib import pylab as pl
import matplotlib as mpl
mpl.rc('font', **{'size':18})

mpl.rcParams['xtick.major.size'] = 7
mpl.rcParams['xtick.major.width'] = 1
mpl.rcParams['xtick.minor.size'] = 3
mpl.rcParams['xtick.minor.width'] = 1
mpl.rcParams['ytick.major.size'] = 7
mpl.rcParams['ytick.major.width'] = 1
mpl.rcParams['ytick.minor.size'] = 3
mpl.rcParams['ytick.minor.width'] = 1

def beta_incomplete(a,b,x):
    return (x**a/a)*hyp2f1(a,1-b,a+1,x)
```

**Parameters:**

Need to specify:
   - `MPBH` , the mass of PBH in solar masses
   - `a` , the semi-major axis of the orbit we're interested in, which determines the size of the DM halo

```python
MPBH = 30
a = 0.01

#Load the values of a, r_tr, z from a file
avals, rvals, zvals = np.loadtxt("distributions/Decoupling_M=" + str(int(MPBH)) + "Msun.txt", unpack=True)
rtr_interp = interp1d(avals, rvals)

#Calculate the corresponding value of rtr
rtr = rtr_interp(a)
print "Truncation radius [pc]:", rtr
#print rtr_interp(0.0896960135445)

r_eq = 0.0063*(MPBH**(1.0/3.0)) #Truncation radius in pc at equality
G = 4.302e-3 #Units of (pc/solar mass) (km/s)^2
A = 3.0*MPBH/(8*np.pi*(rtr*r_eq)**1.5)

```

    Truncation radius [pc]: 0.0043154407521

**DM Density profile:**

Note that the normalisation of the gNFW density profile `rho0` is fixed later on...

```python
x_a = 1.0
rho0 = 1.0

def rho_inner(x):
    return 1.0/((x**(3/2))*(1 + x)**(6-3/2))

alpha = 1.0*rho_inner(x_a)
#beta = (1.0/alpha)*1.0*3*(1+4*x_a)/(2*x_a**(5/2)*(1+x_a)**(11/2))
beta = 3*(1+4*x_a)/(2.0*x_a*(1+x_a))

def rho_outer(x):
    return alpha*sp.exp(-1.0*beta*(x-x_a))

def rho(x):
    if (x < x_a):
        return rho_inner(x)
    if (x >= x_a):
        return rho_outer(x)

def P(x):
    return 4*np.pi*1.0*x**(1/2)/((1 + x)**(4-3/2))

def rho_true(x):
    if (x > 1.0):
        return 0.0
    return A*x**(-1.5)

rho_vec = np.vectorize(rho)

```

**Calculate masses and potentials:**

```python
x1,x2,x3,y = sp.symbols('x1 x2 x3 y')
rho0 = 1.0

def Menc_inner(x):
    return rtr**3*4*np.pi*1.0*2*(x**(3/2))*(35 + 28*x + 8*x**2)/(105.0*(1+x)**(7/2))

Mouter_fun = Menc_inner(x_a) + rtr**3*4*np.pi*sp.integrate(rho_outer(x1)*x1**2, (x1, x_a, x2))
#print Menc_inner(0.5)
#print Mouter_fun.subs(x1,1.5)
rho0 = ((rtr/r_eq)**1.5)*MPBH/Mouter_fun.subs(x2, 100)

def Menc(x):
    if (x < x_a):
        return MPBH + rho0*Menc_inner(x)
    elif (x >= x_a):
        return MPBH + rho0*Mouter_fun.subs(x2, x)

def psi_outer(y):
    return (MPBH + rho0*Menc_inner(x_a))*G/(rtr*y) - (2**(-9/2))*4*np.pi*rtr**3*(G/rtr)*rho0*(np.exp((x_a - y)*beta)*alpha*(2 -2*np.exp(y*beta) + y*beta)/(y*beta**3))

def psi_inner1(y):
    num = 48-70*np.sqrt(y*(1+y)) - 112*np.sqrt(y**3*(1+y))+ \
            -48*np.sqrt(y**5*(1+y)) + 48*y*(3+y*(3+y))
    return (rtr**3*4*np.pi*rho0)*(G/rtr)*2*num/(105.0*(1+y)**3) + MPBH*G/(rtr*y)

def psi_inner(y):
    return psi_inner1(y) - psi_inner1(x_a) + psi_outer(x_a)

def psi(y):
    if (y < x_a):
        return psi_inner(y)
    elif (y >= x_a):
        return psi_outer(y)

psi_tr = float(psi(1.0))
psi_min = float(psi(100))
psi_max = float(psi(1e-6))

```

**Plot the density...**

```python
#Check the density
pl.figure(figsize=(7,5))
xlist = np.logspace(np.log10(1e-5), np.log10(10),1000)
pl.loglog(xlist, np.vectorize(rho_true)(xlist), label='UCMH (Eq. 13)', lw=1.5, color='C3')
pl.loglog([1.0, 1.0], [1e-3, rho_true(1.0)],lw=1.5, color='C3')
pl.loglog(xlist, rho0*np.vectorize(rho)(xlist),linestyle='--',label='gNFW', lw=1.5, color='C9')
#pl.loglog(xlist, rho_inner(xlist))
pl.xlabel(r"$x = r/R_\mathrm{tr}$")
pl.ylabel(r"$\rho_\mathrm{DM}$ [$M_\odot$ pc$^{-3}$]")
pl.ylim(1e5, 1e15)
pl.xlim(1e-4, 10)
pl.axvline(1.0, linestyle=':', color='k')
#pl.axvline(x_b, linestyle=':', color='k')
pl.legend(loc="best",fontsize=16.0)
#pl.xticks([1e-3, 1e-2, 1e-1, 1, 10], [r'$10^{-3}$',r'$10^{-2}$',r'$10^{-1}$',r'$1$',r'$10$'])

pl.tight_layout()
pl.savefig("../plots/Halo_density.pdf",bbox_inches="tight")
pl.show()

#Check the density
pl.figure()
xlist = np.logspace(np.log10(1e-3), np.log10(10),1000)
pl.plot(xlist, rho0*np.vectorize(rho)(xlist)/np.vectorize(rho_true)(xlist),label='gNFW')
#pl.loglog(xlist, rho_inner(xlist))
pl.xlabel(r"$x = r/r_\mathrm{tr}$")
pl.ylabel(r"$\rho/\rho_\mathrm{true}$")
#pl.ylim(0, 1e6)
#pl.xlim(0.5, 5)
pl.axvline(1.0, linestyle=':', color='k')
#pl.axvline(x_b, linestyle=':', color='k')
#pl.legend(loc="best",fontsize=10.0)
#pl.tight_layout()
#pl.savefig("../../Halo_density.pdf",bbox_inches="tight" )
pl.show()
```

**Plot the potential and mass enclosed:**

```python
#Check the potential
print "Potential at x = 100:", psi_min
#print "Potential at x = 1.1:", psi_b
print "Potential at x = 1:", psi_tr
print "Potential at x = 1e-5:", psi_max
print " "
print "Mass enclosed at x = 1:", Menc(1.0)-MPBH
print "Mass enclosed at x = 1.5:", Menc(1.5)-MPBH
print "Mass enclosed at x = 2:", Menc(2)-30
print "Mass enclosed at x = 5:", Menc(5)-30
print "Mass enclosed between at x = 5 and x = 8:", Menc(8)-Menc(5)
print " "
print "Fraction of particles inside x = 1e-2:", (Menc(1e-2)-MPBH)/(Menc(100)-MPBH)

pl.figure()
xlist = np.linspace(1e-3, 5, 100)
pl.loglog(xlist, np.vectorize(psi)(xlist))
pl.xlabel(r"$x = r/r_\mathrm{tr}$")
pl.ylabel(r"$\psi$ [$(\mathrm{km}/\mathrm{s})^2$]")
#pl.axvline(x_a, linestyle=':', color='k')
pl.axvline(1.0, linestyle=':', color='k')
pl.xlim(0.1, 2)
pl.show()

pl.figure()
xlist = np.linspace(0.0, 5, 100)
pl.plot(xlist, np.vectorize(Menc)(xlist))
pl.xlabel(r"$x = r/r_\mathrm{tr}$")
pl.ylabel(r"$M_\mathrm{enc} [M_\odot]$")
pl.axvline(1.0, linestyle=':', color='k')
#pl.axvline(x_b, linestyle=':', color='k')
pl.show()
```

**Calculating density as a function of potential:**

```python
#List of x values
xlist = np.append(np.logspace(-5, 0, 2000),np.linspace(1.10001, 10.0, 1000))
xlist = np.append(xlist, np.linspace(10.1, 100.0, 1000))
xlist = np.append(xlist, np.linspace(0.9, 1.1, 1000))
xlist = np.sort(xlist)

```

```python
rholist = np.vectorize(rho)(xlist)
psilist = np.asarray(np.vectorize(psi)(xlist),dtype='float64')
rholist = np.append(rholist, 0)
psilist = np.append(psilist, 0)
rho_of_psi = UnivariateSpline(psilist[::-1],rholist[::-1],k=5,s=0)
```

```python
drho = rho_of_psi.derivative(n=1)
d2rho = rho_of_psi.derivative(n=2)
```

```python
#c = 14.2844298406921
#def d2rho_fixed_scalar(p):
#    delta = 0.1
#    if (p < -1):
#        return 0.0
#    elif (psi_tr-delta < p <= psi_tr):
#        return d2rho(psi_tr - delta)
#    elif (psi_tr < p < psi_tr + delta):
#        return d2rho(psi_tr + delta)
#    elif (p >= -1):
#        return d2rho(p)

#d2rho_fixed = np.vectorize(d2rho_fixed_scalar)
```

**Calculating $f(\epsilon)$ over a grid of $\epsilon$ values**

```python
def calcf1(eps):
    #if (eps <= psi_min):
    #    return 0.0
    if (eps < 1e-10):
        return 0.0
    integ = lambda p: rho0*d2rho(p)/np.sqrt(eps - p)
    return quad(integ, 0, eps)[0]/(np.sqrt(8)*np.pi**2)
```

```python
e_list = np.append(np.logspace(np.log10(psi_min),np.log10(psi_tr-0.0001), 300),np.logspace(np.log10(psi_tr+0.0001), np.log10(psi_max),300))
e_list = np.sort(np.append(e_list, np.linspace(psi_tr-1.0, psi_tr+1.0, 100)))
e_list = np.append(0, e_list)

```

```python
fvals = 0.0*e_list
for i in tqdm(range(len(e_list))):
    fvals[i] = calcf1(e_list[i])

#print fvals
```

     50%|█████     | 353/701 [00:17<00:17, 20.22it/s]/usr/local/lib/python2.7/site-packages/scipy/integrate/quadpack.py:356: IntegrationWarning: The maximum number of subdivisions (50) has been achieved.
      If increasing the limit yields no improvement it is advised to analyze
      the integrand in order to determine the difficulties.  If the position of a
      local difficulty can be determined (singularity, discontinuity) one will
      probably gain from splitting up the interval and calling the integrator
      on the subranges.  Perhaps a special-purpose integrator should be used.
      warnings.warn(msg, IntegrationWarning)
     98%|█████████▊| 690/701 [02:24<00:02,  4.77it/s]/usr/local/lib/python2.7/site-packages/scipy/integrate/quadpack.py:356: IntegrationWarning: The algorithm does not converge.  Roundoff error is detected
      in the extrapolation table.  It is assumed that the requested tolerance
      cannot be achieved, and that the returned result (if full_output = 1) is
      the best which can be obtained.
      warnings.warn(msg, IntegrationWarning)
    100%|██████████| 701/701 [02:28<00:00,  4.73it/s]

```python
#e_list, fvals = np.loadtxt("distribution_trunc3.dat", unpack=True)
```

```python
finterp = UnivariateSpline(e_list, fvals, k=1,s=0)
def f(eps):
    #if (eps < 1):
    #    return 0.0
    return np.clip(finterp(eps),0,1e30)

f_vec = np.vectorize(f)
```

```python
pl.figure()
pl.loglog(e_list,f_vec(e_list))
#pl.semilogx(e_list,np.vectorize(stepfun)(e_list))
#pl.axvline(psi_tr, linestyle=':', color='k')
#pl.ylim(1, 1e4)
#pl.xlim(0, 20.0)
#print psi(5)
pl.xlabel(r'$\epsilon$')
pl.ylabel(r'$f(\epsilon)$')
pl.axvline(psi_min, color='k', linestyle=":")
pl.axvline(psi_tr, color='k', linestyle=":")
pl.show()
```

**Re-checking the density:**

```python
def vmax(x):
    return np.sqrt(2*float(psi(x)))

def dens(x):
    integ = lambda v: v**2*f(float(psi(x)) - 0.5*v**2)
    return 4*np.pi*quad(integ, 0, vmax(x))[0]
```

```python
xlist2 = np.logspace(np.log10(1e-6), np.log10(10),50)
#xlist2 = np.linspace(0.85, 1.15,10)
dens_list = 0.0*xlist2
for i in tqdm(range(len(xlist2))):
    dens_list[i] = dens(xlist2[i])
```

    100%|██████████| 50/50 [02:06<00:00,  2.52s/it]

```python
pl.figure()
pl.loglog(xlist2, rho0*np.vectorize(rho)(xlist2), label="True")
pl.loglog(xlist2, dens_list, label="Reconstructed")
pl.legend(loc='best',fontsize=12.0)
pl.xlabel(r'$x = r/r_\mathrm{tr}$')
pl.ylabel(r"$\rho_\mathrm{DM}$ [$M_\odot$ pc$^{-3}$]")

pl.figure()
pl.semilogx(xlist2, (1/rho0)*dens_list/np.vectorize(rho)(xlist2))
pl.xlabel(r'$x = r/r_\mathrm{tr}$')
pl.ylabel(r"$\rho_\mathrm{recon}/\rho_\mathrm{true}$")
#pl.ylim(0.99,1.01)
#pl.xlim(0, 2)
pl.show()
```

**Save the distribution function to file**

```python
htxt = "M_PBH = " + str(int(MPBH)) + " M_solar, a/pc = "+"{0:.3f}".format(a) + ", r_tr/pc = " + str(rtr)
outfilename = "distributions/distribution_M=" + str(int(MPBH)) + "_a=" +"{0:.3f}".format(a) + ".dat"

np.savetxt(outfilename,zip(e_list,np.clip(fvals,0,1e30)), header=htxt)

print outfilename
```

    distribution_M=30_a=0.010.dat

**Take a look at the distribution function**

```python
#Speed distribution f(v) at a given radius r
def f_scalar(x, v):
    if (v >= vmax(x)):
        return 0.0
    else:
        return 4.0*np.pi*(v**2)*f(float(psi(x)) - 0.5*v**2)/(rho0*rho(x))

f_v = np.vectorize(f_scalar)
```

```python
xpos = 1e-5
vlist = np.linspace(0, vmax(xpos),100)
#upper = 4*np.pi*vmax(xpos)**2*np.max(fvals[e_list < float(psi(xpos))])/rho(xpos)
#print upper
pl.figure()
pl.plot(vlist, f_v(xpos,vlist))
#pl.axhline(upper, linestyle=':', color='g')
pl.axhline(5.0/vmax(xpos), linestyle=':', color='r')
pl.xlabel(r'$v [\mathrm{km/s}]$')
pl.ylabel(r'$f(v) [\mathrm{km/s}^{-1}]$')
pl.show()

```

```python
print "DONE!"
```

    DONE!


```
%load_ext autoreload
%autoreload 2
```

```
import numpy as np
import matplotlib.pyplot as plt
import common
import sympy as sp

%matplotlib inline
%config InlineBackend.figure_format='retina'

fault_depth = 0.5

def fault_fnc(q):
    return 0 * q, q - 1 - fault_depth, -np.ones_like(q), 0 * q, np.ones_like(q)

surf_L = 10

def flat_fnc(q):
    return surf_L * q, 0 * q, 0 * q, np.ones_like(q), np.full_like(q, surf_L)

def slip_fnc(xhat):
    # This must be zero at the endpoints!
    return np.where(
        xhat < -0.9, (1.0 + xhat) * 10, np.where(xhat < 0.9, 1.0, (1.0 - xhat) * 10)
    )

plt.plot(slip_fnc(np.linspace(-1, 1, 100)))
```

```
qr_fault = common.gauss_rule(50)
fault = fault_fnc(qr_fault[0])

qr_flat = common.gauss_rule(200)
flat = flat_fnc(qr_flat[0])

A, A_info = common.interaction_matrix(
    common.double_layer_matrix, flat, qr_flat, flat, qr_flat
)
B, B_info = common.interaction_matrix(
    common.double_layer_matrix, flat, qr_flat, fault, qr_fault
)
A = A[:, 0, :]
B = B[:, 0, :]
```

```
slip = slip_fnc(qr_fault[0])
v = B.dot(slip)
```

```
surf_disp = np.linalg.solve(A - 0.5 * np.eye(A.shape[0]), v)

plt.plot(surf_disp)
plt.show()
```

```
def hypersingular_matrix(surface, quad_rule, obsx, obsy):
    srcx, srcy, srcnx, srcny, curve_jacobian = surface

    dx = obsx[:, None] - srcx[None, :]
    dy = obsy[:, None] - srcy[None, :]
    r2 = dx ** 2 + dy ** 2

    obsnx = np.full_like(obsx, 1.0)
    obsny = 0.0 * obsx

    srcn_dot_obsn = srcnx[None, :] * obsnx[:, None] + srcny[None, :] * obsny[:, None]
    d_dot_srcn = dx * srcnx[None, :] + dy * srcny[None, :]
    d_dot_obsn = dx * obsnx[:, None] + dy * obsny[:, None]

    # The definition of the hypersingular kernel.
    integrand = (srcn_dot_obsn - (2 * d_dot_srcn * d_dot_obsn / r2)) / (2 * np.pi * r2)

    return integrand * curve_jacobian * quad_rule[1][None, :]
```

```
def interior_eval(
    kernel,
    src_surface,
    src_quad_rule,
    src_slip,
    obsx,
    obsy,
    offset_mult,
    kappa,
    qbx_p,
    visualize_centers=False,
):
    n_qbx = src_surface[0].shape[0] * kappa
    quad_rule_qbx = common.gauss_rule(n_qbx)
    surface_qbx = common.interp_surface(src_surface, src_quad_rule[0], quad_rule_qbx[0])
    slip_qbx = common.interp_fnc(src_slip, src_quad_rule[0], quad_rule_qbx[0])

    qbx_center_x1, qbx_center_y1, qbx_r1 = common.qbx_choose_centers(
        src_surface, src_quad_rule, mult=offset_mult, direction=1.0
    )
    qbx_center_x2, qbx_center_y2, qbx_r2 = common.qbx_choose_centers(
        src_surface, src_quad_rule, mult=offset_mult, direction=-1.0
    )
    qbx_center_x = np.concatenate([qbx_center_x1, qbx_center_x2])
    qbx_center_y = np.concatenate([qbx_center_y1, qbx_center_y2])
    qbx_r = np.concatenate([qbx_r1, qbx_r2])

    if visualize_centers:
        plt.plot(surface_qbx[0], surface_qbx[1], "k-")
        plt.plot(qbx_center_x, qbx_center_y, "r.")
        plt.show()

    Qexpand = common.qbx_expand_matrix(
        kernel,
        surface_qbx,
        quad_rule_qbx,
        qbx_center_x,
        qbx_center_y,
        qbx_r,
        qbx_p=qbx_p,
    )
    qbx_coeffs = Qexpand.dot(slip_qbx)
    disp_qbx = common.qbx_interior_eval(
        kernel,
        src_surface,
        src_quad_rule,
        src_slip,
        obsx,
        obsy,
        qbx_center_x,
        qbx_center_y,
        qbx_r,
        qbx_coeffs,
    )
    return disp_qbx
```

```
nobs = 100
zoomx = [-2.5, 2.5]
zoomy = [-5.1, -0.1]
# zoomx = [-25, 25]
# zoomy = [-45, 5]
xs = np.linspace(*zoomx, nobs)
ys = np.linspace(*zoomy, nobs)
obsx, obsy = np.meshgrid(xs, ys)

disp_flat = interior_eval(
    common.double_layer_matrix,
    flat,
    qr_flat,
    surf_disp,
    obsx.flatten(),
    obsy.flatten(),
    offset_mult=5,
    kappa=2,
    qbx_p=10,
    visualize_centers=True,
).reshape(obsx.shape)

disp_fault = interior_eval(
    common.double_layer_matrix,
    fault,
    qr_fault,
    slip,
    obsx.flatten(),
    obsy.flatten(),
    offset_mult=5,
    kappa=2,
    qbx_p=10,
    visualize_centers=True,
).reshape(obsx.shape)

disp_full = disp_flat + disp_fault

levels = np.linspace(-0.5, 0.5, 21)
cntf = plt.contourf(obsx, obsy, disp_full, levels=levels, extend="both")
plt.contour(
    obsx,
    obsy,
    disp_full,
    colors="k",
    linestyles="-",
    linewidths=0.5,
    levels=levels,
    extend="both",
)
plt.plot(flat[0], flat[1], "k-", linewidth=1.5)
plt.plot(fault[0], fault[1], "k-", linewidth=1.5)
plt.colorbar(cntf)
plt.xlim(zoomx)
plt.ylim(zoomy)
plt.show()
```

```
nobs = 100
zoomx = [-2.5, 2.5]
zoomy = [-5.1, -0.1]
xs = np.linspace(*zoomx, nobs)
ys = np.linspace(*zoomy, nobs)
obsx, obsy = np.meshgrid(xs, ys)

stress_flat = interior_eval(
    common.hypersingular_matrix,
    flat,
    qr_flat,
    surf_disp,
    obsx.flatten(),
    obsy.flatten(),
    offset_mult=5,
    kappa=2,
    qbx_p=10,
).reshape((*obsx.shape, 2))

stress_fault = interior_eval(
    common.hypersingular_matrix,
    fault,
    qr_fault,
    slip,
    obsx.flatten(),
    obsy.flatten(),
    offset_mult=5,
    kappa=2,
    qbx_p=10,
).reshape((*obsx.shape, 2))

stress_full = stress_flat + stress_fault

levels = np.linspace(-0.5, 0.5, 21)
plt.figure(figsize=(8, 4))
for d in range(2):
    plt.subplot(1, 2, 1 + d)
    cntf = plt.contourf(obsx, obsy, stress_full[:, :, d], levels=levels, extend="both")
    plt.contour(
        obsx,
        obsy,
        stress_full[:, :, d],
        colors="k",
        linestyles="-",
        linewidths=0.5,
        levels=levels,
        extend="both",
    )
    plt.plot(flat[0], flat[1], "k-", linewidth=1.5)
    plt.plot(fault[0], fault[1], "k-", linewidth=1.5)
    plt.colorbar(cntf)
    plt.xlim(zoomx)
    plt.ylim(zoomy)
plt.tight_layout()
plt.show()
```


# 03 - Full-Waveform Inversion (FWI)

This notebook is the third in a series of tutorial highlighting various aspects of seismic inversion based on Devito operators. In this second example we aim to highlight the core ideas behind seismic inversion, where we create an image of the subsurface from field recorded data. This tutorial follows on the modelling tutorial and will reuse the modelling and velocity model.

## Inversion requirement

Seismic inversion relies on two known parameters:

- **Field data** - or also called **recorded data**. This is a shot record corresponding to the true velocity model. In practice this data is acquired as described in the first tutorial. In order to simplify this tutorial we will fake field data by modelling it with the true velocity model.

- **Initial velocity model**. This is a velocity model that has been obtained by processing the field data. This model is a rough and very smooth estimate of the velocity as an initial estimate for the inversion. This is a necessary requirement for any optimization (method).

## Inversion computational setup

In this tutorial, we will introduce the gradient operator. This operator corresponds to the imaging condition introduced in the previous tutorial with some minor modifications that are defined by the objective function (also referred to in the tutorial series as the *functional*, *f*) and its gradient, *g*. We will define these two terms in the tutorial too.

## Notes on the operators

As we have already described the creation of a forward modelling operator, we will only call a wrapper function here. This wrapper already contains all the necessary operators for seismic modeling, imaging and inversion. Operators introduced for the first time in this tutorial will be properly described.

```python
import numpy as np
%matplotlib inline

from devito import configuration
configuration['log-level'] = 'WARNING'
```

## Computational considerations

As we will see, FWI is computationally extremely demanding, even more than RTM. To keep this tutorial as lightwight as possible we therefore again use a very small demonstration model. We also define here a few parameters for the final example runs that can be changed to modify the overall runtime of the tutorial.

```python
nshots = 9  # Number of shots to create gradient from
nreceivers = 101  # Number of receiver locations per shot
fwi_iterations = 5  # Number of outer FWI iterations
```

# True and smooth velocity models

We will use a very simple model domain, consisting of a circle within a 2D domain. We will again use the "true" model to generate our synthetic shot data and use a "smooth" model as our initial guess. In this case the smooth model is very smooth indeed - it is simply a constant background velocity without any features.

```python
#NBVAL_IGNORE_OUTPUT
from examples.seismic import demo_model, plot_velocity, plot_perturbation

# Define true and initial model
shape = (101, 101)  # Number of grid point (nx, nz)
spacing = (10., 10.)  # Grid spacing in m. The domain size is now 1km by 1km
origin = (0., 0.)  # Need origin to define relative source and receiver locations

model = demo_model('circle-isotropic', vp_circle=3.0, vp_background=2.5,
                    origin=origin, shape=shape, spacing=spacing, nbl=40)

model0 = demo_model('circle-isotropic', vp_circle=2.5, vp_background=2.5,
                     origin=origin, shape=shape, spacing=spacing, nbl=40,
                    grid = model.grid)

plot_velocity(model)
plot_velocity(model0)
plot_perturbation(model0, model)
```

## Acquisition geometry

In this tutorial, we will use the easiest case for inversion, namely a transmission experiment. The sources are located on one side of the model and the receivers on the other side. This allows to record most of the information necessary for inversion, as reflections usually lead to poor inversion results.

```python
#NBVAL_IGNORE_OUTPUT
# Define acquisition geometry: source
from examples.seismic import AcquisitionGeometry

t0 = 0.
tn = 1000.
f0 = 0.010
# First, position source centrally in all dimensions, then set depth
src_coordinates = np.empty((1, 2))
src_coordinates[0, :] = np.array(model.domain_size) * .5
src_coordinates[0, 0] = 20.  # Depth is 20m

# Define acquisition geometry: receivers

# Initialize receivers for synthetic and imaging data
rec_coordinates = np.empty((nreceivers, 2))
rec_coordinates[:, 1] = np.linspace(0, model.domain_size[0], num=nreceivers)
rec_coordinates[:, 0] = 980.

# Geometry

geometry = AcquisitionGeometry(model, rec_coordinates, src_coordinates, t0, tn, f0=f0, src_type='Ricker')
# We can plot the time signature to see the wavelet
geometry.src.show()
```

```python
# Plot acquisition geometry
plot_velocity(model, source=geometry.src_positions,
              receiver=geometry.rec_positions[::4, :])
```

## True and smooth data

We can generate shot records for the true and smoothed initial velocity models, since the difference between them will again form the basis of our imaging procedure.

```python
# Compute synthetic data with forward operator
from examples.seismic.acoustic import AcousticWaveSolver

solver = AcousticWaveSolver(model, geometry, space_order=4)
true_d, _, _ = solver.forward(vp=model.vp)
```

```python
# Compute initial data with forward operator
smooth_d, _, _ = solver.forward(vp=model0.vp)
```

```python
#NBVAL_IGNORE_OUTPUT
from examples.seismic import plot_shotrecord

# Plot shot record for true and smooth velocity model and the difference
plot_shotrecord(true_d.data, model, t0, tn)
plot_shotrecord(smooth_d.data, model, t0, tn)
plot_shotrecord(smooth_d.data - true_d.data, model, t0, tn)
```

# Full-Waveform Inversion

## Formulation

Full-waveform inversion (FWI) aims to invert an accurate model of the discrete wave velocity, $\mathbf{c}$, or equivalently the square slowness of the wave, $\mathbf{m} = \frac{1}{\mathbf{c}^2}$, from a given set of measurements of the pressure wavefield $\mathbf{u}$. This can be expressed as the following optimization problem [1, 2]:

\begin{aligned}
	\mathop{\hbox{minimize}}_{\mathbf{m}} \Phi_s(\mathbf{m})&=\frac{1}{2}\left\lVert\mathbf{P}_r
	\mathbf{u} - \mathbf{d}\right\rVert_2^2 \\
	\mathbf{u} &= \mathbf{A}(\mathbf{m})^{-1} \mathbf{P}_s^T \mathbf{q}_s,
\end{aligned}

where $\mathbf{P}_r$ is the sampling operator at the receiver locations, $\mathbf{P}_s^T$ is the injection operator at the source locations, $\mathbf{A}(\mathbf{m})$ is the operator representing the discretized wave equation matrix, $\mathbf{u}$ is the discrete synthetic pressure wavefield, $\mathbf{q}_s$ is the corresponding pressure source and $\mathbf{d}$ is the measured data. It is worth noting that $\mathbf{m}$ is the unknown in this formulation and that multiple implementations of the wave equation operator $\mathbf{A}(\mathbf{m})$ are possible.

We have already defined a concrete solver scheme for $\mathbf{A}(\mathbf{m})$ in the first tutorial, including appropriate implementations of the sampling operator $\mathbf{P}_r$ and source term $\mathbf{q}_s$.

To solve this optimization problem using a gradient-based method, we use the
adjoint-state method to evaluate the gradient $\nabla\Phi_s(\mathbf{m})$:

\begin{align}
 \nabla\Phi_s(\mathbf{m})=\sum_{\mathbf{t} =1}^{n_t}\mathbf{u}[\mathbf{t}] \mathbf{v}_{tt}[\mathbf{t}] =\mathbf{J}^T\delta\mathbf{d}_s,
\end{align}

where $n_t$ is the number of computational time steps, $\delta\mathbf{d}_s = \left(\mathbf{P}_r \mathbf{u} - \mathbf{d} \right)$ is the data residual (difference between the measured data and the modelled data), $\mathbf{J}$ is the Jacobian operator and $\mathbf{v}_{tt}$ is the second-order time derivative of the adjoint wavefield solving:

\begin{align}
 \mathbf{A}^T(\mathbf{m}) \mathbf{v} = \mathbf{P}_r^T \delta\mathbf{d}.
\end{align}

We see that the gradient of the FWI function is the previously defined imaging condition with an extra second-order time derivative. We will therefore reuse the operators defined previously inside a Devito wrapper.

## FWI gradient operator

To compute a single gradient $\nabla\Phi_s(\mathbf{m})$ in our optimization workflow we again use `solver.forward` to compute the entire forward wavefield $\mathbf{u}$ and a similar pre-defined gradient operator to compute the adjoint wavefield `v`. The gradient operator provided by our `solver` utility also computes the correlation between the wavefields, allowing us to encode a similar procedure to the previous imaging tutorial as our gradient calculation:

- Simulate the forward wavefield with the background velocity model to get the synthetic data and save the full wavefield $\mathbf{u}$
- Compute the data residual
- Back-propagate the data residual and compute on the fly the gradient contribution at each time step.

This procedure is applied to multiple source positions and summed to obtain a gradient image of the subsurface. We again prepare the source locations for each shot and visualize them, before defining a single gradient computation over a number of shots as a single function.

```python
#NBVAL_IGNORE_OUTPUT

# Prepare the varying source locations sources
source_locations = np.empty((nshots, 2), dtype=np.float32)
source_locations[:, 0] = 30.
source_locations[:, 1] = np.linspace(0., 1000, num=nshots)

plot_velocity(model, source=source_locations)
```

```python
# Create FWI gradient kernel
from devito import Function, TimeFunction
from examples.seismic import Receiver

import scipy
def fwi_gradient(vp_in):
    # Create symbols to hold the gradient and residual
    grad = Function(name="grad", grid=model.grid)
    residual = Receiver(name='rec', grid=model.grid,
                        time_range=geometry.time_axis,
                        coordinates=geometry.rec_positions)
    objective = 0.
    for i in range(nshots):
        # Update source location
        geometry.src_positions[0, :] = source_locations[i, :]

        # Generate synthetic data from true model
        true_d, _, _ = solver.forward(vp=model.vp)

        # Compute smooth data and full forward wavefield u0
        smooth_d, u0, _ = solver.forward(vp=vp_in, save=True)

        # Compute gradient from data residual and update objective function
        residual.data[:] = smooth_d.data[:] - true_d.data[:]

        objective += .5*np.linalg.norm(residual.data.flatten())**2
        solver.gradient(rec=residual, u=u0, vp=vp_in, grad=grad)

    return objective, -grad.data
```

Having defined our FWI gradient procedure we can compute the initial iteration from our starting model. This allows us to visualize the gradient alongside the model perturbation and the effect of the gradient update on the model.

```python
# Compute gradient of initial model
ff, update = fwi_gradient(model0.vp)
assert np.isclose(ff, 57283, rtol=1e0)
```

```python
#NBVAL_IGNORE_OUTPUT
from examples.seismic import plot_image

# Plot the FWI gradient
plot_image(update, vmin=-1e4, vmax=1e4, cmap="jet")

# Plot the difference between the true and initial model.
# This is not known in practice as only the initial model is provided.
plot_image(model0.vp.data - model.vp.data, vmin=-1e-1, vmax=1e-1, cmap="jet")

# Show what the update does to the model
alpha = .5 / np.abs(update).max()
plot_image(model0.vp.data - alpha*update, vmin=2.5, vmax=3.0, cmap="jet")
```

We see that the gradient and the true perturbation have the same sign, therefore, with an appropriate scaling factor, we will update the model in the correct direction.

```python
# Define bounding box constraints on the solution.
def apply_box_constraint(vp):
    # Maximum possible 'realistic' velocity is 3.5 km/sec
    # Minimum possible 'realistic' velocity is 2 km/sec
    return np.clip(vp, 2.0, 3.5)
```

```python
#NBVAL_SKIP

# Run FWI with gradient descent
history = np.zeros((fwi_iterations, 1))
for i in range(0, fwi_iterations):
    # Compute the functional value and gradient for the current
    # model estimate
    phi, direction = fwi_gradient(model0.vp)

    # Store the history of the functional values
    history[i] = phi

    # Artificial Step length for gradient descent
    # In practice this would be replaced by a Linesearch (Wolfe, ...)
    # that would guarantee functional decrease Phi(m-alpha g) <= epsilon Phi(m)
    # where epsilon is a minimum decrease constant
    alpha = .05 / np.abs(direction).max()

    # Update the model estimate and enforce minimum/maximum values
    model0.vp = apply_box_constraint(model0.vp.data - alpha * direction)

    # Log the progress made
    print('Objective value is %f at iteration %d' % (phi, i+1))
```

    Objective value is 57283.142744 at iteration 1
    Objective value is 35764.785716 at iteration 2
    Objective value is 21001.497656 at iteration 3
    Objective value is 11547.509773 at iteration 4
    Objective value is 5785.733449 at iteration 5

```python
#NBVAL_IGNORE_OUTPUT

# Plot inverted velocity model
plot_velocity(model0)
```

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

# Plot objective function decrease
plt.figure()
plt.loglog(history)
plt.xlabel('Iteration number')
plt.ylabel('Misift value Phi')
plt.title('Convergence')
plt.show()
```

## References

[1] _Virieux, J. and Operto, S.: An overview of full-waveform inversion in exploration geophysics, GEOPHYSICS, 74, WCC1–WCC26, doi:10.1190/1.3238367, http://library.seg.org/doi/abs/10.1190/1.3238367, 2009._

[2] _Haber, E., Chung, M., and Herrmann, F. J.: An effective method for parameter estimation with PDE constraints with multiple right hand sides, SIAM Journal on Optimization, 22, http://dx.doi.org/10.1137/11081126X, 2012._

This notebook is part of the tutorial "Optimised Symbolic Finite Difference Computation with Devito" presented at the Intel® HPC Developer Conference 2017.


# Урок первый

## Краткий конспект вводного занятия
* знакомство (с обсуждением того сколько рукопожатий в группе из `n` человек)
* обсуждение того что такое программирование и алгоритмы
* обсуждение методов сортировки построением ребят в ряд и упорядочиванием по росту
* собравшись возле одного компьютера и используя онлайн-интерпретатор мы ввели несколько команд и увидели как интерпретатор языка реагирует на эти команды; например, мы ввели следующее:

```
>>> print("Hello, world!")
Hello World!
>>> print("Hello World! " * 3)
Hello World! Hello World! Hello World!
```
Мы также вывели формулу для подсчета числа рукопожатий в группе из `n` человек. Наша формула выглядела следующим образом:
$$H_n = 1 + 2 + \ldots + (n-1) = H_{n-1} + n-1, \text{ } n=2,3,\ldots,$$
где $H_n$ обозначает число рукопожатий в группе из $n$ человек, и $H_1=0$.

Мы подсчитали число рукопожатий для небольших значений n:

\begin{align}
H_2 & = 1, \nonumber\\
H_3 & = H_2 + 2 = 3, \nonumber\\
H_4 & = H_3 + 3 = 6, \nonumber\\
H_5 & = H_4 + 4 = 10, \nonumber\\
H_6 & = H_5 + 5 = 15, \nonumber\\
\ldots\nonumber
\end{align}

Mы воспользовались выведенной формулой и языком Python, чтобы подсчитать число рукопожатий для больших значений `n`:

```
>>> n = 25
>>> L = list(range(n))
>>> print(L)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]
>>> Hn = str(sum(L))
>>> print('В группе из {0} человек возможны точно {1} рукопожатий.'.format(n, Hn))
В группе из 25 человек возможны точно 300 рукопожатий.
```

**Материалы лекций используемых нами в этом курсе основаны на следующей книге:
Джейсон Бриггс, Python для детей. Самоучитель по программированию; пер. с англ. Станислава Ломакина ; [науч. ред. Д. Абрамова]. — М. : Манн,
Иванов и Фербер, 2017. — 320 с.**

```python

```


# ADG with two real variables and Covariance inequality
**author:Alessio Benavoli**
 alessiobenavoli.com

We will learn how to build a PyRational **ADG (Almost Desirable Gambles)** belief model on the outcome of an experiment whose space of possibility is $\mathbb{R}^2$.

```python
%load_ext autoreload
%autoreload 2
from __future__ import absolute_import
from PyRational.models.ADG import  ADG
from sympy import symbols,  Interval, Piecewise, Eq, exp
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
```

*PyRational* uses *Sympy* for symbolic mathematics. We need to define in *Sympy*  a `symbol` associated to the real variable and its relative domain  (we use Sympy `Interval` for the latter).

```python
x1=symbols('x1', real=True)
x2=symbols('x2', real=True)
domain_x=[Interval(-10,10),Interval(-10,10)]
```

```python
model = ADG([x1,x2],domain_x)
model
```

ADG model

  List of Symbols 	[x1, x2]

  Domain 	Ω=Interval(-10, 10) x Interval(-10, 10)

  List of desirable gambles 	G=[]

  Avoiding sure loss? 	  to be verified

We assume that our agent, Alice, finds the following gambles desirable.

```python
G=[]
G.append( x1)
G.append(-x1)
G.append(x1**2-1)
G.append(1-x1**2)
G.append( x2)
G.append(-x2)
G.append(x2**2-1)
G.append(1-x2**2)
```

We add all these gambles to `model` as follows:

```python
model.add_gambleList(G)
model
```

ADG model

  List of Symbols 	[x1, x2]

  Domain 	Ω=Interval(-10, 10) x Interval(-10, 10)

  List of desirable gambles 	G=[x1, -x1, x1**2 - 1, -x1**2 + 1, x2, -x2, x2**2 - 1, -x2**2 + 1]

  Avoiding sure loss? 	  to be verified

```python
model.Gambles
```

    [x1, -x1, x1**2 - 1, -x1**2 + 1, x2, -x2, x2**2 - 1, -x2**2 + 1]

Note that $G$ is a list that includes all Alice's desirable gambles. We now `build` the belief model
and check if it avods sure loss:

```python
optimoptions={'method_LISP': 'Cutting_plane', #'Cutting_plane', 'discretise'
                       'SolverLP':'linprog', #'linprog', 'cplex'
                       'LP_acc_constraints':1e-8,
                       'SolverNLP':'differential_evolution',
                       'NLP_alpha_cut':-0.00001,
                       'num_support_points': 150,
                       'verbose':False}
model.buildModel(options=optimoptions)
model.check_avs(options=optimoptions)
model
```

    /home/benavoli/anaconda3/lib/python3.6/site-packages/scipy/optimize/_linprog_ip.py:1262: OptimizeWarning: Solving system with option 'sym_pos':True failed. It is normal for this to happen occasionally, especially as the solution is approached. However, if you see this frequently, consider setting option 'sym_pos' to False.
      OptimizeWarning)
    /home/benavoli/anaconda3/lib/python3.6/site-packages/scipy/optimize/_linprog_ip.py:1274: OptimizeWarning: Solving system with option 'sym_pos':False failed. This may happen occasionally, especially as the solution is approached. However, if you see this frequently, your problem may be numerically challenging. If you cannot improve the formulation, consider setting 'lstsq' to True. Consider also setting `presolve` to True, if it is not already.
      OptimizeWarning)

    Belief Model avoids sure  loss

ADG model

  List of Symbols 	[x1, x2]

  Domain 	Ω=Interval(-10, 10) x Interval(-10, 10)

  List of desirable gambles 	G=posi(ℒ(Ω)+      ∪    [x1, x1**2 - 1, x2, x2**2 - 1])

  Avoiding sure loss? 	 Yes

So Alice is **rational** or, equivalently, her set of desirable gambles is coherent.

## Inference

Assume Alice is interested in computing her maximum buying/minimum selling price for the gamble

$$
f=x_1 x_2
$$

 We can do that as follows:

```python
f_range=(None,None)
f=x1*x2
lp=model.lower_prevision(f,f_range,options=optimoptions)
up=model.upper_prevision(f,f_range,options=optimoptions)
print(lp,str("<= E[x_1 x_2] <="), up)
```

    -0.9955920104136098 <= E[x_1 x_2] <= 0.9983673733966469

We have obtained the covariance inequality for standardised variables:

$$
|E(X_1X_2)|^2 \leq E(X_1^2) E(X_2^2).
$$

which implies

$$
-1=-\sqrt{E(X_1^2) E(X_2^2)} \leq E(X_1X_2) \leq \sqrt{E(X_1^2) E(X_2^2)}=1.
$$

## Structual judgments (independence)

Under a judgment of 'independence', if Alice finds the gambles in $G_x$ on $x_1$ desirable and the gambles
in $G_y$ on $x_2$ desirable, she should also find the gambles in $G_x \otimes G_y$ desirable

```python
Gx=[]
Gx.append( Piecewise((1,True)) ) # constant 1
Gx.append( x1)
Gx.append(-x1)
Gx.append(x1**2-1)
Gx.append(1-x1**2)

Gy=[]
Gy.append( Piecewise((1,True)) )
Gy.append( x2)
Gy.append(-x2)
Gy.append(x2**2-1)
Gy.append(1-x2**2)

Gprod=[a*b for a in Gx for b in Gy]
Gprod
```

    [1,
     x2,
     -x2,
     x2**2 - 1,
     -x2**2 + 1,
     x1,
     x1*x2,
     -x1*x2,
     x1*(x2**2 - 1),
     x1*(-x2**2 + 1),
     -x1,
     -x1*x2,
     x1*x2,
     -x1*(x2**2 - 1),
     -x1*(-x2**2 + 1),
     x1**2 - 1,
     x2*(x1**2 - 1),
     -x2*(x1**2 - 1),
     (x1**2 - 1)*(x2**2 - 1),
     (x1**2 - 1)*(-x2**2 + 1),
     -x1**2 + 1,
     x2*(-x1**2 + 1),
     -x2*(-x1**2 + 1),
     (-x1**2 + 1)*(x2**2 - 1),
     (-x1**2 + 1)*(-x2**2 + 1)]

```python
model1 = ADG([x1,x2],domain_x)
model1.add_gambleList(Gprod)
model1.buildModel(options=optimoptions)
model1.check_avs(options=optimoptions)
model1
```

    /home/benavoli/anaconda3/lib/python3.6/site-packages/scipy/optimize/_linprog_ip.py:1262: OptimizeWarning: Solving system with option 'sym_pos':True failed. It is normal for this to happen occasionally, especially as the solution is approached. However, if you see this frequently, consider setting option 'sym_pos' to False.
      OptimizeWarning)
    /home/benavoli/anaconda3/lib/python3.6/site-packages/scipy/optimize/_linprog_ip.py:1274: OptimizeWarning: Solving system with option 'sym_pos':False failed. This may happen occasionally, especially as the solution is approached. However, if you see this frequently, your problem may be numerically challenging. If you cannot improve the formulation, consider setting 'lstsq' to True. Consider also setting `presolve` to True, if it is not already.
      OptimizeWarning)

    Belief Model avoids sure  loss

ADG model

  List of Symbols 	[x1, x2]

  Domain 	Ω=Interval(-10, 10) x Interval(-10, 10)

  List of desirable gambles 	G=posi(ℒ(Ω)+      ∪    [1, x2, x2**2 - 1, x1, x1*x2, x1*(x2**2 - 1), x1*(-x2**2 + 1), x1**2 - 1, x2*(x1**2 - 1), (x1**2 - 1)*(x2**2 - 1), (x1**2 - 1)*(-x2**2 + 1), x2*(-x1**2 + 1), (-x1**2 + 1)*(x2**2 - 1), (-x1**2 + 1)*(-x2**2 + 1)])

  Avoiding sure loss? 	 Yes

```python
f_range=(None,None)
f=x1*x2
lp=model1.lower_prevision(f,f_range,options=optimoptions)
up=model1.upper_prevision(f,f_range,options=optimoptions)
print(lp,str("<= E[x_1 x_2] <="), up)
```

    /home/benavoli/anaconda3/lib/python3.6/site-packages/scipy/optimize/_linprog_ip.py:1262: OptimizeWarning: Solving system with option 'sym_pos':True failed. It is normal for this to happen occasionally, especially as the solution is approached. However, if you see this frequently, consider setting option 'sym_pos' to False.
      OptimizeWarning)
    /home/benavoli/anaconda3/lib/python3.6/site-packages/scipy/optimize/_linprog_ip.py:1274: OptimizeWarning: Solving system with option 'sym_pos':False failed. This may happen occasionally, especially as the solution is approached. However, if you see this frequently, your problem may be numerically challenging. If you cannot improve the formulation, consider setting 'lstsq' to True. Consider also setting `presolve` to True, if it is not already.
      OptimizeWarning)

    9.996338666551363e-09 <= E[x_1 x_2] <= -9.998929295651493e-09

This time $E[x_1 x_2]=0$ which follows from independence


| |Pierre Proulx, ing, professeur|
|:---|:---|
|Département de génie chimique et de génie biotechnologique |** GCH200-Phénomènes d'échanges I **|

### Section18.7, Difffusion et réaction dans un catalyseur poreux sphérique

Voir les hypothèses dans le livre de Transport Phenomena.

```python
#
# Pierre Proulx
#
# Préparation de l'affichage et des outils de calcul symbolique
#
import sympy as sp
from IPython.display import *
sp.init_printing(use_latex=True)
%matplotlib inline
```

```python
# Paramètres, variables et fonctions
r,delta_r,R,k_1,a,D_A,C_AR=sp.symbols('r,delta_r,R,k_1,a,D_A,C_AR')
N_A=sp.symbols('N_A')
C_A=sp.Function('C_A')(r)
f=sp.Function('f')(r)
```

```python
#
# Résultat du bilan sur une coquille sphérique
#
eq=sp.Eq(sp.diff(N_A(r)*r**2,r)+k_1*a*C_A*r**2,0)
display(eq)
eq=eq.subs(N_A(r),-D_A*sp.diff(C_A))
display(eq)
### suggestion de transformation faite dans Bird pour solutionner
eqt=sp.Eq(f.diff(r,r)-k_1*a/D_A*f,0)
display(eqt)
```

```python
t=sp.dsolve(eqt)
display(t)
display(t.subs(f,C_A/C_AR*r))         # car f = r * C_A/C_AR
C=sp.symbols('C')   # C est défini comme C_A/C_AR ici
C=(t.rhs/r).simplify()
display(C)                    # cette expression doit être équivalente à 18-7.8
```

```python
#
# Solution après transformation, avant les conditions aux limites (annexe C-1)
#
C1,C2=sp.symbols('C1,C2')
C_A=C_AR*C1/r*sp.cosh((k_1*a/D_A)**0.5*r)+C_AR*C2/r*sp.sinh((k_1*a/D_A)**0.5*r)
display(C_A)
```

```python
#C_A=C_A.subs(sp.symbols('C1'),0)
cl1=r**2*sp.diff(C_A,r)
cl1=sp.cancel(cl1) #pour faire effectuer les multiplications et divisions de r et r2
cl1=cl1.subs(r,0)
cl2=sp.Eq(C_A.subs(r,R)-C_AR,0)    # à la surface, C_A = C_AR
cl2=cl2.lhs
constantes=sp.solve([cl1,cl2],sp.symbols('C1 C2'))
display(constantes)
C_A=C_A.subs(constantes)
display(sp.simplify(C_A))
```

```python
#
# Tracer le profil pour différentes valeurs de la constante de réaction
#
dico1={'C_AR':1,'D_A':1.e-7,'R':0.01,'a':16,'k_1':1.e-5}
Thiele1=((k_1*a/D_A)**(0.5)*R).subs(dico1).evalf(3)
C_A1=C_A.subs(dico1)
dico2={'C_AR':1,'D_A':1.e-7,'R':0.01,'a':16,'k_1':4.e-5}
Thiele2=((k_1*a/D_A)**(0.5)*R).subs(dico2).evalf(3)
C_A2=C_A.subs(dico2)
dico3={'C_AR':1,'D_A':1.e-7,'R':0.01,'a':16,'k_1':16.e-5}
Thiele3=((k_1*a/D_A)**(0.5)*R).subs(dico3).evalf(3)
C_A3=C_A.subs(dico3)
dico4={'C_AR':1,'D_A':1.e-7,'R':0.01,'a':16,'k_1':64.e-5}
Thiele4=((k_1*a/D_A)**(0.5)*R).subs(dico4).evalf(3)
C_A4=C_A.subs(dico4)
dico5={'C_AR':1,'D_A':1.e-7,'R':0.01,'a':16,'k_1':256.e-5}
Thiele5=((k_1*a/D_A)**(0.5)*R).subs(dico5).evalf(3)
C_A5=C_A.subs(dico5)
import matplotlib.pyplot as plt
plt.rcParams['figure.figsize']=10,8
p = sp.plot((C_A1,(r,0,0.01)),(C_A2,(r,0,0.01)),(C_A3,(r,0,0.01)),(C_A4,(r,0,0.01)),(C_A5,(r,0,0.01)),
            legend=True, title='Concentration radiale en fonction de Thiele',
            xlabel='r',ylabel='C_a',show=False)
p[0].line_color = 'blue'
p[0].label=str(Thiele1)
p[1].line_color = 'black'
p[1].label=str(Thiele2)
p[2].line_color = 'red'
p[2].label=str(Thiele3)
p[3].line_color = 'green'
p[3].label=str(Thiele4)
p[4].line_color = 'yellow'
p[4].label=str(Thiele5)
p.show()
```


# Contents
1. [勾配法](#%E5%8B%BE%E9%85%8D%E6%B3%95)
    1. [1変数関数](#1%E5%A4%89%E6%95%B0%E9%96%A2%E6%95%B0)
    1. [多変数関数](#%E5%A4%9A%E5%A4%89%E6%95%B0%E9%96%A2%E6%95%B0)

# 勾配法（最急降下法、山登り法）
gradientが最大になる方向へステップを刻む
1. 勾配$\nabla f$を与える必要があるので微分できない関数には使えない
1. 最大値・最小値以外に極値が存在する場合、初期値によっては最大値・最小値以外の極値に到達する可能性がある
1. 極値が1つしかない場合でも、関数形によっては計算効率が悪くなる（勾配がなくなるので）

## 1変数関数
__Algorithm 3.1__

```julia
using PyPlot
using LinearAlgebra
using Printf
```

```julia
function sgn(X)
    if X < 0
        return -1.0
    elseif X==0
        return 0.0
    else
        return 1.0
    end
end

function search(X₀, f, f_diff; δ=1.0, h₀=1.0)
    X=X₀
    X_steps = Array([[X]])
    while norm(f_diff(X))>=δ
        h=sgn.(f_diff(X)).*abs(h₀)
        X1=X
        X2=X.+h
        if f(X1)<f(X2)
            while f(X1)<f(X2)
                h=2h
                X1=X2
                X2= X1.+h
            end
            X=X1
            h=h./2.0
        else
            while f(X1)>f(X2)
                h=h./2.0
                X2=X2.-h
            end
            X=X2
            h=2 .* h
        end
        append!(X_steps, [[X]])
    end
    return X, Array(transpose(hcat(X_steps...)))
end
```

    search (generic function with 1 method)

以下の関数の最大値をとるxを数値的に求める\*
$$
f(x) = -\left(x^2 +10 \sin (x)\right)
$$
最大値をとるxは大体1.3くらい。
\*[scipy lecture note](http://www.turbare.net/transl/scipy-lecture-notes/intro/scipy.html)より

```julia
f(x)=-(x^2.0 + 10.0*sin(x))
f_diff(x)=-(2.0*x + 10.0*cos(x))
x=range(-10,stop=10,length=1000)
δ=1e-2

x₀=8.0
h₀_list=range(0.2,stop=1.0, length=5)
figsize = (20,3)
fig,axes=subplots(1,5,figsize=figsize)
for (i,h₀) in enumerate(h₀_list)
    x_estimated,x_steps=search(x₀, f, f_diff, h₀=h₀, δ=δ)
    ax=axes[i]
    ax[:set_title](L"$f(x)$, $h_0=$"* @sprintf("%2.2f",h₀))
    ax[:plot](x, f.(x), color="k", lw=1)
    ax[:plot](x_steps, f.(x_steps), color="r", lw=1, ls="--", marker="o", markersize=3)
end

x₀=1.0
h₀_list=range(0.2,stop=1.0, length=5)
figsize = (20,3)
fig,axes=subplots(1,5,figsize=figsize)
for (i,h₀) in enumerate(h₀_list)
    x_estimated,x_steps=search(x₀, f, f_diff, h₀=h₀)
    ax=axes[i]
    ax[:set_title](L"$f(x)$, $h_0=$"* @sprintf("%2.2f",h₀))
    ax[:plot](x, f.(x), color="k", lw=1)
    ax[:plot](x_steps, f.(x_steps), color="r", lw=1, ls="--", marker="o", markersize=3)
end
```

初期値の値によって異なる極値へ到達してしまうことがわかる。

## 多変数関数
__Algorithm 3.2__
多変数関数でも基本的に1変数関数の場合と同様。
$\nabla f$の方向の直線状で関数値が最大になる点まで進むことを繰り返す（直線探索）

6つこぶラクダの背関数\*の最小値を求める（つもりだった）。
$$
\begin{align}
f(x, y) &= (4 - 2.1x^2 + \frac{x^4}{3})x^2 + xy + (4y^2 - 4)y^2 \\
\nabla f &= \left(\begin{array}{c}
\frac{\partial f}{\partial x} \\
\frac{\partial f}{\partial y}
\end{array}\right)
= \left(\begin{array}{c}
8x-8.4x^3 + 2x^5+y \\
x+16y^3-8y
\end{array}\right)
\end{align}
$$
\*[scipy lecture note](http://www.turbare.net/transl/scipy-lecture-notes/intro/scipy.html)より

```julia
# 最大値を探しに行くアルゴリズムなので符号反転した
f(x) = ((4-2.1*x[1]^2 + (x[1]^4)/3)*x[1]^2 + x[1]*x[2] + (4x[2]^2-4)*x[2]^2) .* (-1.0)
∇f(x) = [8x[1]-8.4x[1]^3+2x[1]^5+x[2]; x[1]+16x[2]^3-8x[2]] .*(-1.0)
```

    ∇f (generic function with 1 method)

まずは関数をプロットして概観をつかむ。

```julia
xx=collect(range(-2,stop=2,length=100))
yy=collect(range(-1,stop=1,length=100))
z=zeros(length(xx),length(yy))

for (i,x) in enumerate(xx)
    for (j,y) in enumerate(yy)
        z[i,j]=f([x,y])
    end
end

figure()
surf(xx,yy,z, cmap="viridis")
ax=gca()
ax[:set_xlabel]("X")
ax[:set_ylabel]("y")
ax[:set_zlabel]("z")

figure()
_cp=contour(xx,yy,z, colors="k", lw=1, levels=10)
ax=gca()
ax[:clabel](_cp, inline=1, fontsize=10)
ax[:set_xlabel]("X")
ax[:set_ylabel]("y")

```

```julia
function hill_climbing(x₀,t₀,δ,f,∇f;h₀=1.0)
    x=x₀
    x_steps=[x]
    t=t₀
    Δx=[Inf,Inf]
    while norm(Δx)>=δ
        F(t)=f(x.+t.*∇f(x))
        dFdt(t)=dot(∇f(x.+t.*∇f(x) ), ∇f(x))
        t_estimated,t_steps=search(t,F,dFdt,h₀=h₀)
        Δx=t_estimated.*∇f(x)
        x=x.+Δx
        append!(x_steps,[x])
        t=t_estimated
    end
    return x, Array(transpose(hcat(x_steps...)))
end

x₀=[1.5, 0.0]   #[-3,3]より外に出ると計算時間が爆発（勾配消失？）
t₀=0.01
h₀=1.0
δ=1e-8

x,x_steps=hill_climbing(x₀,t₀,δ,f,∇f,h₀=h₀)

figure()
_cp=contour(xx,yy,z, colors="k", lw=1, levels=10)
ax=gca()
ax[:clabel](_cp, inline=1, fontsize=10)
ax[:set_xlabel]("X")
ax[:set_ylabel]("y")
ax[:scatter](x[1], x[2], c="r")
ax[:scatter](x₀[1], x₀[2])
```

青が始点、赤が終点である。上記の関数だとうまく計算ができなかった（複雑な多峰だから？）。
変数変換なんかせずに実装したほうが素直で良いかもしれないが、今回はsearch関数を教科書通りに再利用した。

簡単な関数なら上手く行くだろうということで、次に単峰の2次元ガウス関数で試した。
式は以下の通り。
$$
\begin{align}
f(\boldsymbol{x}) &= \exp\left(-\frac{1}{2}(\boldsymbol{x}-\boldsymbol{\mu})^\mathrm{T}Σ^{-1}(\boldsymbol{x}-\boldsymbol{\mu})\right) \\
\nabla f(\boldsymbol{x}) &= \exp\left(-\frac{1}{2}(\boldsymbol{x}-\boldsymbol{\mu})^\mathrm{T}Σ^{-1}(\boldsymbol{x}-\boldsymbol{\mu})\right) \cdot \left(-Σ^{-1}(\boldsymbol{x}-\boldsymbol{\mu})\right) \\
\boldsymbol{\mu} &= \left(\begin{array}{c}
0 \\
0
\end{array}\right),\;\;
\Sigma=\left(\begin{array}{cc}
1 & 0 \\
0 & 2
\end{array}\right)
\end{align}
$$

まずは可視化

```julia
μ=[0.0, 0.0]
Σ=[1.0 0.0; 0.0 2.0]
Σ_inv=inv(Σ)
f(x)=exp(-(x-μ)'*Σ_inv*(x-μ) * 0.5)
∇f(x)=exp(-(x-μ)'*Σ_inv*(x-μ) * 0.5) * (-Σ_inv*(x-μ))

xx=collect(range(-3,stop=3,length=100))
yy=collect(range(-3,stop=3,length=100))
z=zeros(length(xx),length(yy))

for (i,x) in enumerate(xx)
    for (j,y) in enumerate(yy)
        z[i,j]=f([x,y])
    end
end

figure()
surf(xx,yy,z, cmap="viridis", rcount=100, ccount=100, shade=true)
ax=gca()
ax[:set_xlabel]("X")
ax[:set_ylabel]("y")
ax[:set_zlabel]("z")

figure()
_cp=contour(xx,yy,z, colors="k", lw=1, levels=5)
ax=gca()
ax[:clabel](_cp, inline=1, fontsize=10)
ax[:set_xlabel]("X")
ax[:set_ylabel]("y")
```

__Algorithm 3.2__の実装

```julia
function hill_climbing(x₀,t₀,δ,f,∇f;h₀=1.0)
    x=x₀
    x_steps=[x]
    t=t₀
    Δx=[Inf,Inf]
    while norm(Δx)>=δ
        F(t)=f(x.+t.*∇f(x))
        dFdt(t)=dot(∇f(x.+t.*∇f(x) ), ∇f(x))
        t_estimated,t_steps=search(t,F,dFdt,h₀=h₀)
        Δx=t_estimated.*∇f(x)
        x=x.+Δx
        append!(x_steps,[x])
        t=t_estimated
    end
    return x, Array(transpose(hcat(x_steps...)))
end
```

    hill_climbing (generic function with 1 method)

計算

```julia
x₀=[3.0, 3.0]
t₀=0.1
h₀=1.0
δ=1e-8
x,x_steps=hill_climbing(x₀,t₀,δ,f,∇f,h₀=h₀)
```

    ([4.80335e-15, 1.8349e-7], [3.0 3.0; 2.99965 2.99982; … ; 5.33705e-15 1.93148e-7; 4.80335e-15 1.8349e-7])

プロット

```julia
figure()
_cp=contour(xx,yy,z, colors="k", lw=1, levels=5)
ax=gca()
ax[:clabel](_cp, inline=1, fontsize=10)
ax[:set_xlabel]("X")
ax[:set_ylabel]("y")
ax[:scatter](x_steps[:,1], x_steps[:,2], s=1)
lim_x=xlim()
lim_y=ylim()
ax[:set_aspect]((lim_x[2]-lim_x[1])/(lim_y[2]-lim_y[1]))
```

とりあえず上手く行ったようである。

実際扱う問題では解析的に微分ができるようなことは稀である。
そこで、偏微分計算も数値的に実装してみた。
（現在地点から少しだけ座標を動かして関数値の変化分を計算するだけの簡単実装）

```julia
# 偏微分
function numeric_partial_diff(f, X, i; h=1e-4)
    h_vec = zero(X)
    h_vec[i] = h
    return (f(X.+h_vec)-f(X.-h_vec)) / (2.0*h)
end

# 勾配
function grad(f, X; h=1e-4)
    ∇f=zero(X)
    for i in 1:length(X)
        ∇f[i]=numeric_partial_diff(f,X,i,h=h)
    end
    return ∇f
end
```

    grad (generic function with 1 method)

計算

```julia
μ=[0.0, 0.0]
Σ=[1.0 0.0; 0.0 2.0]
Σ_inv=inv(Σ)
f(x)=exp(-(x-μ)'*Σ_inv*(x-μ) * 0.5)

X₀=[3.0, 3.0]   #[-3,3]より外に出ると計算時間が爆発（勾配消失？）
h₀=1.0
t₀=0.1
δ=1e-8
∇f(X) = grad(f, X)

x,x_steps=hill_climbing(X₀,t₀,δ,f,∇f,h₀=h₀)
```

    ([-7.32747e-15, 1.8349e-7], [3.0 3.0; 2.99965 2.99982; … ; -7.32747e-15 1.93147e-7; -7.32747e-15 1.8349e-7])

プロット

```julia
figure()
_cp=contour(xx,yy,z, colors="k", lw=1, levels=5)
ax=gca()
ax[:clabel](_cp, inline=1, fontsize=10)
ax[:set_xlabel]("X")
ax[:set_ylabel]("y")
ax[:scatter](x_steps[:,1], x_steps[:,2], s=1)
lim_x=xlim()
lim_y=ylim()
ax[:set_aspect]((lim_x[2]-lim_x[1])/(lim_y[2]-lim_y[1]))
```

どうやら上手く行ったようだ。
めでたしめでたし。

```julia

```


# Latent Factor DCMM Analysis

This example highlights several advanced modeling strategies.

**Dynamic Count Mixture Model**

A Dynamic Count Mixture Model (DCMM) is the combination of Bernoulli and Poisson DGLMs. The Bernoulli DGLM models the probability of a zero outcome, while the Poisson models the outcome, conditional on it being non-zero (Berry and West, 2019):

\begin{equation} \label{eqn-dcmm}
z_t \sim Ber(\pi_t) \text{ and } y_t \mid z_t =
\begin{cases}
0, & \text{if } z_t = 0,\\
1 + x_t, \quad x_t \sim Po(\mu_t), & \text{if }z_t = 1
\end{cases}
\end{equation}

where $\pi_t$ and $\mu_t$ vary according to the dynamics of independent Bernoulli and Poisson DGLMs respectively:

\begin{equation*}
\text{logit}(\pi_t) = \mathbf{F}_{ber, t}^{'}\boldsymbol{\theta}_{ber, t} \qquad \text{and} \qquad \text{log}(\mu_t) = \mathbf{F}_{Po, t}^{'} \boldsymbol{\theta}_{Po, t}
\end{equation*}

A DCMM is useful to capture a higher prevalence of $0$ outcomes than in a standard Poisson DGLM.

**Latent Factors**

The second concept is the use of *latent factors* for multiscale modeling. This example features simulated data from a retail sales setting. There is data for an item, and also for total sales at the store. The total sales are smoother and more predictable than the item level sales. A model is fit to the total sales which includes a day-of-week seasonal effect. This seasonal effect is extracted from the model on total sales, and used as a predictor in the item level model.

Modeling effects at different levels of a hierarchy - aka multiscale modeling - can improve forecast accuracy, because the sales of an individual can be very noisy, making it difficult to learn accurate patterns.

**Copula Forecasting**

The final concept is Copula forecasting. In this example we are focusing on forecasting sales *1* through *14* days into the future. To do this, we will simulate from the joint forecast distribution *1:14* days into the future. This is accomplished in an efficient manner through the use of a Copula model, which accounts for dependence in the forecasts across days.

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

```
from pybats_nbdev.analysis import analysis, analysis_dcmm
from pybats_nbdev.latent_factor import seas_weekly_lf
from pybats_nbdev.shared import load_dcmm_latent_factor_example
from pybats_nbdev.plot import plot_data_forecast
from pybats_nbdev.point_forecast import median
from pybats_nbdev.loss_functions import MAD
```

We start by loading the simulated retail sales data.

The dataframe 'totaldata' has the total daily sales in a store, along with a predictor, which represents a measure of average price.

The dataframe 'data' has the sales of a single item, and along with a predictor which represents the daily change in price.

```
data = load_dcmm_latent_factor_example()
totaldata, data = data.values()
totaldata['Y'] = np.log(totaldata['Y'] + 1)
```

```
totaldata.head()
```

      Y
      X

      2014-06-01
      5.613128
      -0.133124

      2014-06-02
      5.602119
      -0.070102

      2014-06-03
      5.831882
      1.020901

      2014-06-04
      5.393628
      2.219605

      2014-06-05
      5.267858
      -0.970556

```
data.head()
```

      Y
      X

      2014-06-01
      14.0
      2.413448

      2014-06-02
      13.0
      0.989871

      2014-06-03
      7.0
      -0.489734

      2014-06-04
      34.0
      -1.544868

      2014-06-05
      15.0
      1.639670

Here we define hyper parameters for the analysis:
-  *rho* is a discount factor which calibrates the forecast distribution. A value of *rho* less than $1$ increases the forecast variance.

- *k* is the number of days ahead to forecast.

- *nsamps* is the number of forecast samples to draw.

- *prior_length* is the number of days of data to use when defining the priors for model coefficients.

```
#Define hyper parameters
rho = .2
k = 14 # Number of days ahead that we will forecast
nsamps = 100
prior_length = 21
```

Next, we define the window of time that we want to forecast over. For each day within this time period, the model will sample from the path (joint) forecast distribution *1:k* days into the future.

```
# Define forecast range for final year of data
T = len(totaldata)
forecast_end_date = totaldata.index[-k]
forecast_start_date = forecast_end_date - pd.DateOffset(days=365)
```

The first analysis is run on the 'totaldata', which is used to learn a latent factor. In this case, we fit a normal DLM to the log of total sales in order to learn the weekly season effect, which is stored in the latent factor.

```
# Get multiscale signal (a latent factor) from higher level log-normal model
latent_factor = analysis(totaldata['Y'].values, totaldata['X'].values, k,
                         forecast_start_date, forecast_end_date, dates=totaldata.index,
                         seasPeriods=[7], seasHarmComponents=[[1,2,3]],
                         family="normal", ret=['new_latent_factors'], new_latent_factors= [seas_weekly_lf],
                         prior_length=prior_length)
```

    beginning forecasting

The second analysis fits a DCMM to 'data', which is the simulated sales of a single item. The latent factor we derived above is used as a predictive signal for the sales of this individual item. On each day within the forecasting period, a copula is used to draw joint forecast samples *1:k* days ahead.

```
# Update and forecast the model
mod, forecast_samples = analysis_dcmm(data['Y'].values, data['X'].values.reshape(-1,1), k,
                                      forecast_start_date, forecast_end_date,
                                      prior_length=prior_length, nsamps=nsamps, rho=rho,
                                      latent_factor=latent_factor, dates=totaldata.index)

forecast = median(forecast_samples)
```

    beginning forecasting

Finally we can examine the results, first by plotting both the *1-* and *14-* step ahead forecasts. The point forecast is the median (blue line), and the forecast samples provide easy access to 95% credible intervals.

```
# Plotting 1-step ahead forecasts

horizon = 1
plot_length = 50
fig, ax = plt.subplots(figsize=(8,4))
start_date = forecast_end_date + pd.DateOffset(horizon - plot_length)
end_date = forecast_end_date + pd.DateOffset(horizon - 1)
ax = plot_data_forecast(fig, ax, data.loc[start_date:end_date].Y,
                        forecast[-plot_length:,horizon - 1],
                        forecast_samples[:,-plot_length:,horizon - 1],
                        data.loc[start_date:end_date].index,
                        linewidth = 2)
```

```
# Plotting 14-step ahead forecasts

horizon = 14
plot_length = 50
fig, ax = plt.subplots(figsize=(8,4))
start_date = forecast_end_date + pd.DateOffset(horizon - plot_length)
end_date = forecast_end_date + pd.DateOffset(horizon - 1)
ax = plot_data_forecast(fig, ax, data.loc[start_date:end_date].Y,
                        forecast[-plot_length:,horizon - 1],
                        forecast_samples[:,-plot_length:,horizon - 1],
                        data.loc[start_date:end_date].index,
                        linewidth = 2)
```

To evaluate the point forecasts, we can look at the mean absolute deviation (MAD) between the forecast median and the observations across forecast horizons. Interestingly, there is only a small increase in the MAD for longer forecast horizons.

```
# Mean absolute deviation at increasing forecast horizons
horizons = list(range(1, k+1))
list(map(lambda k: MAD(data.loc[forecast_start_date + pd.DateOffset(k-1):forecast_end_date + pd.DateOffset(k-1)].Y,
                      forecast[:,k-1]),
         horizons))
```

    [2.8825136612021858,
     2.8579234972677594,
     2.8333333333333335,
     2.8688524590163933,
     2.8852459016393444,
     2.8579234972677594,
     2.8524590163934427,
     2.842896174863388,
     2.92896174863388,
     2.8647540983606556,
     2.8579234972677594,
     2.879781420765027,
     2.8551912568306013,
     2.907103825136612]

```

```

examples/DCMM Latent Factor Example.ipynb


```python
import numpy as np
import time
len_deck = 119315717514047
index_final = 2020
shuffle_time = 101741582076661

puzzle_input = """cut -4258
deal with increment 71
cut -6593
deal into new stack
deal with increment 54
cut -5397
deal into new stack
cut 1327
deal with increment 20
deal into new stack
deal with increment 45
cut -9986
deal into new stack
deal with increment 47
cut -3318
deal with increment 75
cut 542
deal with increment 48
cut 8670
deal with increment 13
deal into new stack
deal with increment 5
cut -8813
deal with increment 36
cut 3228
deal with increment 21
cut 5143
deal with increment 13
cut 7329
deal with increment 74
deal into new stack
deal with increment 4
cut 4178
deal with increment 29
cut -7664
deal with increment 17
cut 8216
deal with increment 22
cut -7497
deal with increment 10
cut -2813
deal into new stack
cut 8416
deal with increment 16
cut -4124
deal with increment 13
cut -8531
deal with increment 74
cut -9397
deal with increment 57
cut -1832
deal with increment 34
cut -2538
deal into new stack
cut 7837
deal with increment 57
cut 5257
deal with increment 2
cut -8241
deal with increment 26
deal into new stack
deal with increment 39
cut -659
deal with increment 58
cut 34
deal into new stack
deal with increment 46
cut 9168
deal with increment 35
cut 8530
deal into new stack
cut 297
deal into new stack
cut 1116
deal with increment 69
cut 5440
deal with increment 6
deal into new stack
cut 3811
deal with increment 7
deal into new stack
cut -8657
deal with increment 29
cut 8933
deal with increment 4
cut -6643
deal with increment 37
cut 1688
deal with increment 32
cut -554
deal with increment 69
deal into new stack
deal with increment 64
cut 4395
deal with increment 71
cut -9180
deal with increment 60
cut 6480
deal with increment 73
cut -7146"""
```

```python

def create_deck(size):
    return np.arange(size, dtype=np.int8)

#deal into new stack
#deal with increment 64
#cut 4395

def deck_to_str(deck):
    ret = ""
    for card in deck:
        ret +=" "
        ret += str(card)
    return ret.strip()

def new_stack(deck, DBG=True):
    if(DBG): print("new stack")
    ret = np.flip(deck,axis=0)
    return ret

def increment(deck, inc, DBG=True):
    if(DBG): print("inc", inc)
    nb_cards = len(deck)
    ret = np.zeros(nb_cards, np.int8)
    index=0
    for card in deck:
        ret[index%nb_cards] = card
        index += inc
    return ret

def cut(deck, inc, DBG=True):
    if(DBG): print("cut", inc)
    ret = np.concatenate([deck[inc:] , deck[:inc]])
    return ret

def function(deck, ccc, DBG=True):

    for line in ccc.splitlines():
        if(DBG): print("line",line)
        if (line== "deal into new stack"):
            deck = new_stack(deck,DBG)
        elif (line.startswith("deal with increment ")):
            inc = int(line[line.rfind(" "):])
            deck = increment(deck, inc,DBG)
        elif (line.startswith("cut ")):
            inc = int(line[line.rfind(" "):])
            deck = cut(deck, inc,DBG)
        else: print("WRONG INSTRUCTION")

    return deck

```

```python

# returns index of card that ends up at index_card_after after "new stack"
def reverse_new_stack(len_deck, index_card_after, DBG=True):
    ret = len_deck - index_card_after - 1
    return ret

# returns index of card that ends up at index_card_after after "increment inc"
def reverse_increment(len_deck, index_card_after, inc, DBG=True):
    if(DBG): print("inc", inc)
    ret=0
    for k in range(len_deck):
        if (k*len_deck+index_card_after)%inc==0:
            ret = (k*len_deck+index_card_after)//inc
            break
    if(DBG): print("ret", ret)
    return ret

# returns index of card that ends up at index_card_after after "cut int"
def reverse_cut(len_deck, index_card_after, inc, DBG=True):
    if(DBG): print("cut", inc)
    ret = (index_card_after + inc) % len_deck
    return ret

def input_to_commands(ccc, DBG=True):
    ret= []
    for line in ccc.splitlines():
        if(DBG): print("line",line)
        if (line== "deal into new stack"):
            ret.append(("STACK",0))
        elif (line.startswith("deal with increment ")):
            inc = int(line[line.rfind(" "):])
            ret.append(("INC",inc))
        elif (line.startswith("cut ")):
            inc = int(line[line.rfind(" "):])
            ret.append(("CUT",inc))
        else: print("WRONG INSTRUCTION")
    return ret

def reverse_function(len_deck, index_card_after, ccc, DBG=True):
    ret = index_card_after
    for command in reversed(ccc):
        if(DBG): print("line",line)
        if (command[0]== "STACK"):
            ret = reverse_new_stack(len_deck, ret, DBG)
        elif (command[0]=="INC"):
            inc = command[1]
            ret = reverse_increment(len_deck, ret, inc, DBG)
        elif (command[0]=="CUT"):
            inc = command[1]
            ret = reverse_cut(len_deck, ret, inc, DBG)
        else: print("WRONG INSTRUCTION")
    return ret

```

```python
#first try

raise

# no success

def test(cc=None, expected=None, DBG=True):
    #deck = create_deck(10)
    len_deck = 10
    ret_deck = []
    commands = input_to_commands(cc)
    for index_card_after in range(len_deck):
        ret = reverse_function(len_deck, index_card_after, commands, DBG)
        ret_deck.append(ret)
    result = deck_to_str(ret_deck)
    expected = str(expected)
    flag = (result == expected)
    print("***"+str(cc) + " -> "+str(result), " -> "+ str(flag) + " vs " + expected)
```

```python
#first try

raise

# no success

print("***new stack")
before = [0,1,2,3,4,5,6,7,8,9]
after = [9,8,7,6,5,4,3,2,1,0]
print(after)
ss=""
for i in range(10):
    pi = reverse_new_stack(10,i)
    print(pi, end='')
    ss += str(before[pi])
print()
print(ss)
print("***")

print("***cut 3")
before = [0,1,2,3,4,5,6,7,8,9]
after = [3,4,5,6,7,8,9,0,1,2]
print(after)
ss=""
for i in range(10):
    pi = reverse_cut(10,i,3,False)
    print(pi, end='')
    ss += str(before[pi])
print()
print(ss)
print("***")

print("***cut -4")
before = [0,1,2,3,4,5,6,7,8,9]
after = [6, 7, 8, 9, 0, 1, 2, 3, 4, 5]
print(after)
ss=""
for i in range(10):
    pi = reverse_cut(10,i,-4,False)
    print(pi, end='')
    ss += str(before[pi])
print()
print(ss)
print("***")

print("***inc 3")
before = [0,1,2,3,4,5,6,7,8,9]
after = [0, 7, 4, 1, 8, 5, 2, 9, 6, 3]
print(after)
ss=""
for i in range(10):
    pi = reverse_increment(10,i,3,False)
    print(pi, end='')
    ss += str(before[pi])
print()
print(ss)
print("***")

print("***inc 7")
before = [0,1,2,3,4,5,6,7,8,9]
after = [0, 3, 6, 9, 2, 5, 8, 1, 4, 7]
print(after)
ss=""
for i in range(10):
    pi = reverse_increment(10,i,7,False)
    print(pi, end='')
    ss += str(before[pi])
print()
print(ss)
print("***")

t1="""deal with increment 7
deal into new stack
deal into new stack"""
test(t1, "0 3 6 9 2 5 8 1 4 7", False)
#Result: 0 3 6 9 2 5 8 1 4 7

t2="""cut 6
deal with increment 7
deal into new stack"""
test(t2,"3 0 7 4 1 8 5 2 9 6",False)
#Result: 3 0 7 4 1 8 5 2 9 6

t31="""deal with increment 7"""
test(t31,"0 3 6 9 2 5 8 1 4 7",False)
#Result: 0 3 6 9 2 5 8 1 4 7

t311="""deal with increment 9"""
test(t311,"0 9 8 7 6 5 4 3 2 1",False)
#Result: 0 9 8 7 6 5 4 3 2 1

t32="""deal with increment 7
deal with increment 9"""
test(t32,"0 7 4 1 8 5 2 9 6 3",False)
#Result: 0 7 4 1 8 5 2 9 6 3

t3="""deal with increment 7
deal with increment 9
cut -2"""
test(t3,"6 3 0 7 4 1 8 5 2 9",False)
#Result: 6 3 0 7 4 1 8 5 2 9

t4="""deal into new stack
cut -2
deal with increment 7
cut 8
cut -4
deal with increment 7
cut 3
deal with increment 9
deal with increment 3
cut -1"""
test(t4,"9 2 5 8 1 4 7 0 3 6",False)
#Result: 9 2 5 8 1 4 7 0 3 6

```

```python
#first try

raise

# no success

%%time
import time

commands = input_to_commands(puzzle_input, False)

len_deck = 119315717514047
ret = reverse_function(len_deck, 2020, commands, False)
print("***",ret,"***") # DO IT MMMM TIMES

cache = {}

ret = 2020
start = time.time()
for k in range(101741582076661):
    if (k%10000==0):
        print(str(k)+"/"+str(101741582076661)+" = "+ str(k/101741582076661*100) + "% - ret=" + str(ret))
        end = time.time()
        print("time in s", (end - start))
        start = end
    ret = reverse_function(len_deck, ret, commands, False)
    if ret in cache:
        print("****PERIODICITY*** k="+str(k) + "ret= "+str(ret)+ "****")
        break
    cache[ret]=k

print("***",ret,"***") # DO IT MMMM TIMES

```

```python
#first try

raise

# no success

%%time
inc = 9
lend = 10
for i in range(lend):
    r=0
    for k in range(lend):
        if (k*lend+i)%inc==0:
            r = (k*lend+i)//inc
            break
    print (i,r)

```

```python
# second try
raise
# no success

#deal with increment 7
#deal into new stack
#deal into new stack
#Result: 0 3 6 9 2 5 8 1 4 7
tt = np.arange(10)
ret = np.zeros(10,int)
for i in range(10):
    j = 10-1-(10-1-((i*7)%10))
    ret[j]=tt[i]
print (ret)

#cut 6
#deal with increment 7
#deal into new stack
#Result: 3 0 7 4 1 8 5 2 9 6
tt = np.arange(10)
ret = np.zeros(10,int)
for i in range(10):
    j = 10-1-((((i-6)%10)*7)%10)
    ret[j]=tt[i]
print (ret)
#raise

#deal with increment 7
#deal with increment 9
#cut -2
#Result: 6 3 0 7 4 1 8 5 2 9
tt = np.arange(10)
ret = np.zeros(10,int)
for i in range(10):
    j = (((((i*7)%10)*9)%10)+2)%10
    ret[j]=tt[i]
print (ret)
#raise

#deal into new stack
#cut -2
#deal with increment 7
#cut 8
#cut -4
#deal with increment 7
#cut 3
#deal with increment 9
#deal with increment 3
#cut -1
#Result: 9 2 5 8 1 4 7 0 3 6
tt = np.arange(10)
ret = np.zeros(10,int)
for i in range(10):
    j = ((((((((10-1-i)+2)*7)-8+4)*7)-3)*9*3)+1)%10
    ret[j]=tt[i]
print (ret)

```

```python
# second try
raise
# no success

from sympy import *
from sympy.parsing.sympy_parser import parse_expr

def expr(ccc,len_deck,DBG=True):
    ret = "idx"

    for line in ccc.splitlines():
        if(DBG): print("line",line)
        if (line== "deal into new stack"):
            ret = str(len_deck)+"-1-("+ret+")"
        elif (line.startswith("deal with increment ")):
            inc = int(line[line.rfind(" "):])
            ret = "("+ret+")*("+str(inc)+")"
        elif (line.startswith("cut ")):
            inc = int(line[line.rfind(" "):])
            ret = "("+ret+")-("+str(inc)+")"
        else: print("WRONG INSTRUCTION")
    ret = "("+ret+")%"+str(len_deck)
    print(ret)
    return ret

def expr_sympy(ccc,len_deck,shuffle_time,DBG=True):
    idx = Symbol("idx")
    ret = idx
    for line in ccc.splitlines():
        if(DBG): print("line",line)
        if (line== "deal into new stack"):
            ret = len_deck-1-ret
        elif (line.startswith("deal with increment ")):
            inc = int(line[line.rfind(" "):])
            ret = ret*inc
        elif (line.startswith("cut ")):
            inc = int(line[line.rfind(" "):])
            ret = ret-inc
        else: print("WRONG INSTRUCTION")
    for k in range(shuffle_time):
        print(k,ret)
        ret = ret.subs(idx,ret)

    ret = ret%len_deck
    print(ret)
    return ret

```

```python
# second try
raise
# no success

t4="""deal into new stack
cut -2
deal with increment 7
cut 8
cut -4
deal with increment 7
cut 3
deal with increment 9
deal with increment 3
cut -1"""
expression_t4 = expr(t4,10)
print(expression_t4)
#Result: 9 2 5 8 1 4 7 0 3 6

expression = expr(puzzle_input,len_deck)
print(expression)

```

```python
# second try
raise
# no success

from sympy import *
from sympy.parsing.sympy_parser import parse_expr
idx = symbols('idx')
init_printing(use_unicode=True)

e = parse_expr(expression)
print(e)

tt = np.arange(10)
ret = np.zeros(10,int)
for kk in range(10):
    j = e.subs(idx, kk)
    ret[j]=tt[kk]
print (ret)

```

```python
# second try
raise
# no success

from sympy import *
from sympy.parsing.sympy_parser import parse_expr
idx = symbols('idx')
init_printing(use_unicode=True)

expression_sympy = expr_sympy(puzzle_input,len_deck,shuffle_time)
print(expression_sympy)

#for i in range(10):
#    expression_sympy = expression_sympy.subs(idx, expression_sympy).simplify()
#    print (i, expression_sympy)
#raise
#tt = np.arange(len_deck)
#ret = np.zeros(len_deck,int)
for kk in range(len_deck):

    rkk = kk
    for i in range(101741582076661):
        rkk = expression_sympy.subs(idx, rkk)
    print("ret[j]^10=rkk",kk,rkk)
#print (ret)

```

```python

def expr_3(ccc,len_deck,DBG=True):
    a=1
    b=0

    for line in ccc.splitlines():
        if(DBG): print("line",line)
        if (line== "deal into new stack"):
            a = -a
            b = (-b+len_deck-1) % len_deck
        elif (line.startswith("cut ")):
            inc = int(line[line.rfind(" "):])
            a = a
            b = (b-inc) % len_deck
        elif (line.startswith("deal with increment ")):
            inc = int(line[line.rfind(" "):])
            a = (inc*a) % len_deck
            b = (inc*b) % len_deck
        else: print("WRONG INSTRUCTION")
    if(DBG):print(a,b)
    return a,b

def apply(a,b,val,len_deck):
    return (((a%len_deck)*val%(len_deck))%len_deck+(b%len_deck))%len_deck

def test_3(cc, expected, DBG):
    a,b = expr_3(cc,10,DBG)
    tt = np.arange(10)
    ret = np.zeros(10,int)
    for kk in range(10):
        j = apply(a,b,kk,10)
        ret[j]=tt[kk]
    if(DBG):print (ret)
    result = deck_to_str(ret)
    expected = str(expected)
    flag = (result == expected)
    print("***R\n"+str(cc) + " -> "+str(result), " -> "+ str(flag) + " vs " + expected+"\n***\n")

t01="""deal into new stack"""
test_3(t01, "9 8 7 6 5 4 3 2 1 0", False)

t02="""cut 3"""
test_3(t02, "3 4 5 6 7 8 9 0 1 2", False)

t03="""cut -4"""
test_3(t03, "6 7 8 9 0 1 2 3 4 5", False)

t04="""deal with increment 3"""
test_3(t04, "0 7 4 1 8 5 2 9 6 3", False)

t1="""deal with increment 7
deal into new stack
deal into new stack"""
test_3(t1, "0 3 6 9 2 5 8 1 4 7", False)
#Result: 0 3 6 9 2 5 8 1 4 7

t2="""cut 6
deal with increment 7
deal into new stack"""
test_3(t2,"3 0 7 4 1 8 5 2 9 6",False)
#Result: 3 0 7 4 1 8 5 2 9 6

t31="""deal with increment 7"""
test_3(t31,"0 3 6 9 2 5 8 1 4 7",False)
#Result: 0 3 6 9 2 5 8 1 4 7

t311="""deal with increment 9"""
test_3(t311,"0 9 8 7 6 5 4 3 2 1",False)
#Result: 0 9 8 7 6 5 4 3 2 1

t32="""deal with increment 7
deal with increment 9"""
test_3(t32,"0 7 4 1 8 5 2 9 6 3",False)
#Result: 0 7 4 1 8 5 2 9 6 3

t3="""deal with increment 7
deal with increment 9
cut -2"""
test_3(t3,"6 3 0 7 4 1 8 5 2 9",False)
#Result: 6 3 0 7 4 1 8 5 2 9

t4="""deal into new stack
cut -2
deal with increment 7
cut 8
cut -4
deal with increment 7
cut 3
deal with increment 9
deal with increment 3
cut -1"""
test_3(t4,"9 2 5 8 1 4 7 0 3 6",False)
#Result: 9 2 5 8 1 4 7 0 3 6

```

    ***R
    deal into new stack -> 9 8 7 6 5 4 3 2 1 0  -> True vs 9 8 7 6 5 4 3 2 1 0
    ***

    ***R
    cut 3 -> 3 4 5 6 7 8 9 0 1 2  -> True vs 3 4 5 6 7 8 9 0 1 2
    ***

    ***R
    cut -4 -> 6 7 8 9 0 1 2 3 4 5  -> True vs 6 7 8 9 0 1 2 3 4 5
    ***

    ***R
    deal with increment 3 -> 0 7 4 1 8 5 2 9 6 3  -> True vs 0 7 4 1 8 5 2 9 6 3
    ***

    ***R
    deal with increment 7
    deal into new stack
    deal into new stack -> 0 3 6 9 2 5 8 1 4 7  -> True vs 0 3 6 9 2 5 8 1 4 7
    ***

    ***R
    cut 6
    deal with increment 7
    deal into new stack -> 3 0 7 4 1 8 5 2 9 6  -> True vs 3 0 7 4 1 8 5 2 9 6
    ***

    ***R
    deal with increment 7 -> 0 3 6 9 2 5 8 1 4 7  -> True vs 0 3 6 9 2 5 8 1 4 7
    ***

    ***R
    deal with increment 9 -> 0 9 8 7 6 5 4 3 2 1  -> True vs 0 9 8 7 6 5 4 3 2 1
    ***

    ***R
    deal with increment 7
    deal with increment 9 -> 0 7 4 1 8 5 2 9 6 3  -> True vs 0 7 4 1 8 5 2 9 6 3
    ***

    ***R
    deal with increment 7
    deal with increment 9
    cut -2 -> 6 3 0 7 4 1 8 5 2 9  -> True vs 6 3 0 7 4 1 8 5 2 9
    ***

    ***R
    deal into new stack
    cut -2
    deal with increment 7
    cut 8
    cut -4
    deal with increment 7
    cut 3
    deal with increment 9
    deal with increment 3
    cut -1 -> 9 2 5 8 1 4 7 0 3 6  -> True vs 9 2 5 8 1 4 7 0 3 6
    ***

```python
from sympy import mod_inverse

a,b = expr_3(puzzle_input,len_deck,False)
print(a,b)

n = shuffle_time
p = len_deck
# a_n = a^n mod p
# b_n = b * sum(k=0,n-1)a^k = b*(1 - a^n)/(1-a)
a_n = pow(a,n,p)

# 2020 = a_n * iiii + b_n
# iiii = (2020 - b_n) / a_n
# iiii = (2020 - b * (a^n mod p - 1) / (a-1)) / (a^n)
# use mod inverse -> yields
# iiii = (2020 - (pow(a, n, p) - 1) * mod_inverse(a-1, p)) * mod_inverse( pow (a,n,p) ,p) % p

iiii = (2020 - b * (pow(a, n, p) - 1) * mod_inverse(a - 1, p)) *mod_inverse(pow(a, n, p),p) % p
print(iiii)

```

    71438317141334 54483033434509
    46116012647793

```python

```