```python
# Importing packages
import matplotlib.pyplot as plt
import pandas as pd
import ipywidgets as widgets
import numpy as np
import itertools
from ipywidgets import Layout
import seaborn as sns
import math

import time
from scipy import linalg
import scipy.optimize as optimize
import sympy as sm

from tkinter import *
import tkinter as tk

from data_gen import gen_df

```

In this project, we want to try to estimate the ratings of movies. That is, we well calculate the utility of a movie, given genres, duration, awards and so forth, and we will try to calculate the IMDB ratings for each movie, based on this utility. Finally, we will then minimize the distance between our estimated ratings and the actual ratings.

Our utility function is given by:
$$ U_i = \sum_{k=1}^{23}(\alpha_k G_{ik}) + \sum_{k=1920s}^{2010s} (\beta_k D_{ik}) + \gamma N_i + \delta W_i + \rho L_i $$.

Based on this utility function, we will estimate the ratings of each movies,
$$ R_i^{model} = \frac{\exp(\omega x_i)}{1 + \exp(\omega x_i)} $$

where,
$$ x_i = \big[G_1, G_2, ..., G_n, D_{1920}, D_{1930}, ..., D_{2010}, N_i, W_i, L_i \big] $$
$$ \omega = \big[\alpha_1, \alpha_2, ..., \alpha_{23}, \beta_{1920s}, \beta_{1930s}, ..., \beta_{2010s}, \gamma, \delta, \rho  \big] $$

We then use optimize methods to solve the following:

$$ \min_{\omega} \Big\{ \sum_{i=1}^{n} \left( R_i^{model} - R_i^{data} \right)^2 \Big\} $$

### The following code will optimize for each decade and plot all estimates

```python
decade_list = [1920, 1930, 1940, 1950, 1960, 1970, 1980, 1990, 2000, 2010]

vars = ['Action','Adventure','Animation','Biography','Comedy','Crime','Documentary',
        'Drama','Family','Fantasy','FilmNoir','History','Horror','Music','Musical',
        'Mystery','Romance','SciFi','Short','Sport','Thriller','War','Western',
        'nrOfNominations','nrOfWins','duration']
```

```python
## The following code define a function that generates two dataframe,
## one with the x-variables and one with the y-variable. The dataframe
## with x-variables only contain movies from the specified decade.

def df_dec(decade):
    """ Generates two dataframe, one with the x-variables (for the specified decade) and one with the y-variable

    Args:
    decade (int): The decade should be named as either 1920, 1930, 1940, 1950, 1960, 1970, 1980, 1990, 2000 or 2010

    Returns:
    Two dataframes.

    """

    # Import the IMDB dataset with information of ratings and X-variables
    df = gen_df('imdb.csv')

    # Dropping movies with less than 5000 ratings
    df = df.loc[(df['ratingCount']>=5000)]

    # Dropping genres with less than 20 obervations
    df.drop(columns=['Adult','GameShow','News','RealityTV','TalkShow'], inplace=True)

    # Keeps movies from the specified decade
    df = df.loc[df['decade'] == f'{decade}s']

    df_X = df.copy() # Creating the dataframe for the x-variables

    # Rearrange columns and keep the specified variables
    df_X = df_X.reindex(['Action','Adventure','Animation','Biography','Comedy','Crime','Documentary',
                              'Drama','Family','Fantasy','FilmNoir','History','Horror','Music','Musical',
                              'Mystery','Romance','SciFi','Short','Sport','Thriller','War','Western',
                              'nrOfNominations','nrOfWins','duration'], axis=1)

    # Creates a dataframe with the observed ratings
    df_Y = pd.DataFrame(df[['imdbRating','index']].copy())
    df_Y.rename(columns = {'imdbRating':'rat_data'}, inplace=True) # Rename variable

    return df_X, df_Y # Return the two dataframes
```

```python
## The following code produces the parameter estimates where the sample is restricted to all the decades,
## one at the time. The function return a list which contains a list of the estimates for every decade.

## OBS: The code will take about 3-4 minutes to run due to the model is estimated 10 times!!

def optimizer_dec():
    """ Generates a list containing 10 list with estimates of the model for every decade.

    Args:
        No arguments are needed.

    Returns:
        A list.

    Notice:
        It will raise an error if the function are given an argument!
    """

    # Defining a function to calculate the sum of squared differences
    # between the ratings from the data and from the model.
    def sqr_diff_sum(df_X,pars):
        """ Generates a float of the sum of squared differences between the ratings from the data and the model.

        Args:
            df_X (DataFrame): DataFrame containing the variable in x for all observations.
            pars (List): List of parameters in omega

        Returns:
            A float.
        """

        util = df_X@pars  # Calculate the matrix product between omega and X
        df_Y['rat_model'] = 10*np.exp(util)/(1+np.exp(util)) # Scale the product so is between 0 and 10. This is the R_model
        df_Y['sqr_diff'] = (df_Y['rat_model']-df_Y['rat_data'])**2 # Calculate the squared difference between R_data and R_model
        return df_Y['sqr_diff'].sum() # Returns the sum of the squared differences

    result = [] # Emty list to store the estimated parameters

    # Make a loop so the model is estimated for all ten decade
    for decade in decade_list:
        df_X, df_Y = df_dec(decade) # Call the function to generate the two dataframes
        x0 = np.zeros(len(vars)) # Starting values

        obj_fun = lambda x: sqr_diff_sum(df_X,x) # The objective function -> sum of squared differences

        # Use Scipy optimizer to solve the model
        result_i = optimize.minimize(obj_fun,x0,
                               method='Nelder-Mead',
                               options={"disp":True, "maxiter":50000}, # display the results
                               );

        # Add the result for each deacde to the result-list
        result.append(list(result_i.x))

    return result # Returns the result-list

# Call the optimize_dec function
result_dec = optimizer_dec()
```

    Optimization terminated successfully.
             Current function value: 5.649208
             Iterations: 10176
             Function evaluations: 12236
    Optimization terminated successfully.
             Current function value: 20.618022
             Iterations: 7768
             Function evaluations: 9337
    Optimization terminated successfully.
             Current function value: 23.340167
             Iterations: 7874
             Function evaluations: 9474
    Optimization terminated successfully.
             Current function value: 81.101081
             Iterations: 11566
             Function evaluations: 13883
    Optimization terminated successfully.
             Current function value: 114.676976
             Iterations: 7507
             Function evaluations: 9034
    Optimization terminated successfully.
             Current function value: 165.183683
             Iterations: 6818
             Function evaluations: 8234
    Optimization terminated successfully.
             Current function value: 401.211190
             Iterations: 7408
             Function evaluations: 8973
    Optimization terminated successfully.
             Current function value: 698.471642
             Iterations: 11452
             Function evaluations: 13733
    Optimization terminated successfully.
             Current function value: 1164.868912
             Iterations: 7780
             Function evaluations: 9371
    Optimization terminated successfully.
             Current function value: 412.781524
             Iterations: 9187
             Function evaluations: 11048

```python
## The following code produces a figure with the estimated parameters for each deacde for a chosen variable

result_dec_mod = [] # Empty list to storage

# The loop changes the order of the result-list so the
# estimates are ordered by the variables and subordered by decade
# insted of being ordered by decade and subordered by variables
for j,var in enumerate(vars):
    temp = []
    for i,dec in enumerate(decade_list):
        temp.append(result_dec[i][j])

    result_dec_mod.append(temp)

# Defining a figure to plot the estimates
def fig(var):
    """ Generates a figure which plots estimated parameters for each decade for one variable

    Args:
        var (string): Should be one of the variables in the X-vector

    Returns:
        One interactive plot.

    Notice:
        The function is generated so that it can be called using widgets.interact.
        Thus, it is not intended to be used on its own.
        """
    fig = plt.figure(figsize=(8,6))
    ax1 = fig.add_subplot(1,1,1)

    ax1.bar(decade_list, result_dec_mod[vars.index(var)],width=6)

    # Setting labels, ticks etc.
    ax1.set_ylabel('Parameter estimates')
    ax1.set_title(f'Parameter estimates for {var} per decade')
    ax1.set_xticks(decade_list)
    ax1.axhline(y=0,color='black',linewidth=1)

# Making the figure interactive so the estimates are shown for the chosen variable
widgets.interact(fig,
    var = widgets.Dropdown(description='Variable', value='Action', options=vars,
                ),
);
```

## The following code will produce a plot with estimate for each parameter

```python
## Fra Sebs kode

def df_s(keep_top=None):
    global df
    global decade_list

    filename = 'imdb.csv'

    df = gen_df(filename)

    decade_list = [1920, 1930, 1940, 1950, 1960, 1970, 1980, 1990, 2000, 2010]

    # Decade dummies
    for i in decade_list:
        df[f'decade_{i}'] = 0
        df.loc[(df['decade'] == f'{i}s'),f'decade_{i}'] = 1

    df = df.loc[(df['ratingCount']>=5000)]
    df.drop(columns=['Adult','GameShow','News','RealityTV','TalkShow'], inplace=True)

    if keep_top != None:
        df = df.sort_values('imdbRating', ascending=False)
        df = df.iloc[:keep_top]

    df_X = df.copy()
    df_Y = pd.DataFrame(df['imdbRating'].copy())
    df_Y.rename(columns = {'imdbRating':'rat_data'}, inplace=True)

    drops = ['index', 'tid', 'title', 'imdbRating', 'ratingCount', 'year',
             'nrOfGenre', 'nrOfPhotos', 'nrOfNewsArticles',  'nrOfUserReviews', 'decade']

    for i in drops:
        df_X.drop(columns=[i], inplace=True)

    # Rearrange columns
    df_X = df_X.reindex(['Action','Adventure','Animation','Biography','Comedy','Crime','Documentary',
                              'Drama','Family','Fantasy','FilmNoir','History','Horror','Music','Musical',
                              'Mystery','Romance','SciFi','Short','Sport','Thriller','War','Western',
                              'decade_1920','decade_1930','decade_1940','decade_1950','decade_1960',
                              'decade_1970','decade_1980','decade_1990','decade_2000','decade_2010',
                              'nrOfNominations','nrOfWins','duration'], axis=1)

    return df_X, df_Y
```

```python
## Fra Sebs kode

vars2 = ['Action','Adventure','Animation','Biography','Comedy','Crime','Documentary',
                          'Drama','Family','Fantasy','FilmNoir','History','Horror','Music','Musical',
                          'Mystery','Romance','SciFi','Short','Sport','Thriller','War','Western',
                          'decade_1920','decade_1930','decade_1940','decade_1950','decade_1960',
                          'decade_1970','decade_1980','decade_1990','decade_2000','decade_2010',
                          'nrOfNominations','nrOfWins','duration']

def optimizer(keep_top=None):
    global fs
    global evals
    global x0
    global df

    def sqr_diff_sum(df_X,pars):
        util = df_X@pars
        df_Y['rat_model'] = 10*np.exp(util)/(1+np.exp(util))
        df_Y['sqr_diff'] = (df_Y['rat_model']-df_Y['rat_data'])**2
        return df_Y['sqr_diff'].sum()

    # Scipy minimize
    def zeros(n):
        list = [0] * n
        return list

    df_X, df_Y = df_s(keep_top)
    x0 = zeros(len(vars2))
    evals = 0

    obj_fun = lambda x: sqr_diff_sum(df_X,x)

    result = optimize.minimize(obj_fun,x0,
                               method="Nelder-Mead",
                               options={"disp":True, "maxiter":50000}, # display the results
                               )

    return result

results = [] # Empty list to store results

# This loop solve the model for all movies and the top 500, 1000, and 2000 movies
for i in [None, 500, 1000, 2000]:
    res_temp = optimizer(keep_top=i)
    temp = res_temp.x
    results.append(temp) # Store the results in the 'results-list'
```

    Optimization terminated successfully.
             Current function value: 3735.382286
             Iterations: 16566
             Function evaluations: 19203
    Optimization terminated successfully.
             Current function value: 213.265323
             Iterations: 17235
             Function evaluations: 19895
    Optimization terminated successfully.
             Current function value: 582.831479
             Iterations: 16138
             Function evaluations: 18644
    Optimization terminated successfully.
             Current function value: 895.336144
             Iterations: 19477
             Function evaluations: 22457
    [array([-0.02249121, -0.03089684,  0.17149985, -0.17152826, -0.02084888,
           -0.03048913,  0.11009148,  0.12785315, -0.06051079, -0.32745048,
            0.02199167, -0.07578793, -0.02139979, -0.03056553, -0.08347352,
           -0.09390543, -0.03941612, -0.09520167,  0.12192359, -0.06356771,
           -0.06100096,  0.19041382,  0.03038359, -0.04148942,  0.12249617,
            0.17989531, -0.03093947,  0.30781892,  0.09645697,  0.00852061,
           -0.2156607 , -0.2024172 , -0.21196039,  0.00395598,  0.01588309,
            0.45167799]), array([-0.11877128, -0.10281598,  0.37591941, -0.34640937,  0.44963963,
            0.01393978,  0.18374984,  0.30143242, -0.1955955 , -0.10595112,
           -0.23594299,  0.36870347, -0.5404849 , -0.13993417, -0.40877283,
           -0.10383054, -0.12874325,  0.13003553,  0.10715166, -0.21497945,
            0.17173981, -0.06567007,  0.37923244,  0.11617889, -0.17575408,
            0.01725559, -0.10747867,  0.06821961, -0.08781796, -0.13053973,
           -0.05139849, -0.03877125,  0.19630215, -0.00240662,  0.00274183,
            0.68502714]), array([ 0.1704907 , -0.03096311, -0.10803699, -0.09170437,  0.23683231,
           -0.24947417,  0.05282689,  0.28486822,  0.38642667, -0.02400058,
           -0.01892814, -0.05772398, -0.21109486, -0.09879497, -0.22273195,
            0.31960029,  0.07178682,  0.32504088,  0.09569002,  0.31668506,
           -0.06381533, -0.68753283, -0.19333604, -0.21751563,  0.34852917,
            0.13482503,  0.01268436, -0.13773108, -0.03321873,  0.00730996,
           -0.40534081,  0.02219064, -0.33867179, -0.00593339,  0.01095337,
            0.65302193]), array([-0.33121987, -0.20515105,  0.3966847 , -0.08547312, -0.037707  ,
            0.01622495,  0.15708472,  0.12690045, -0.06592735,  0.41234624,
           -0.10957163, -0.36811232,  0.29573276, -0.22474644,  0.13497452,
           -0.09475096,  0.05859084, -0.07161956, -0.17934684, -0.05673295,
            0.06807332, -0.18819276, -0.50668664, -0.03372878, -0.12263855,
            0.0367653 ,  0.09566702,  0.13520308, -0.00541802,  0.40282482,
            0.08692355,  0.10161355,  0.12506686, -0.00192629,  0.00361094,
            0.58200293])]
    [-0.11877128 -0.10281598  0.37591941 -0.34640937  0.44963963  0.01393978
      0.18374984  0.30143242 -0.1955955  -0.10595112 -0.23594299  0.36870347
     -0.5404849  -0.13993417 -0.40877283 -0.10383054 -0.12874325  0.13003553
      0.10715166 -0.21497945  0.17173981 -0.06567007  0.37923244  0.11617889
     -0.17575408  0.01725559 -0.10747867  0.06821961 -0.08781796 -0.13053973
     -0.05139849 -0.03877125  0.19630215 -0.00240662  0.00274183  0.68502714]

```python
## The code creates an interactive plot of the estimated parameters for all variables.
## In the interactive part you can choose between estimates when the model is solved
## for all movies or just top top 500, 1000, or 2000 rated movies.

def fig_2(val):
    """ Generates a figure which plots estimated parameters for all variables.

    Args:
        val (string or int): Should be one of the elements in the options-list

    Returns:
        One interactive plot.

    Notice:
        The function is generated so that it can be called using widgets.interact.
        Thus, it is not intended to be used on its own.
    """
    fig = plt.figure(figsize=(12,6))
    ax1 = fig.add_subplot(1,1,1)

    # Plots the estimated parameters for the chosen sample of movies
    ax1.bar(vars2, results[options.index(val)], label=f'Estimates with {options[options.index(val)]} movies')

    # Scatter plot with the estimated paramters for the entire sample
    ax1.scatter(vars2, results[0], marker='D', s=15, zorder=2, label='Estimates with all movies')

    # Legends and labels
    ax1.legend(loc="lower right")
    ax1.set_ylabel('Parameter estimates')
    ax1.set_title(f'Parameter estimates for {options[options.index(val)]} movies')
    ax1.set_ylim([-0.7,0.7])
    ax1.axhline(y=0,color='black',linewidth=1)
    for tick in ax1.get_xticklabels():
        tick.set_rotation(90)

options = ['All', 500, 1000, 2000] # Option list

# Making the figure interactive so the estimates are shown for the chosen sample size
widgets.interact(fig_2,
    val = widgets.Dropdown(description='Nr. of movies', value='All', options=options,
                ),
);
```

    interactive(children=(Dropdown(description='Nr. of movies', options=('All', 500, 1000, 2000), value='All'), Ou…

```python

```


```python
import ipywidgets as widgets
import matplotlib.pyplot as plt
import numpy as np
from scipy import optimize
import sympy as sym

import models
import symbolics
```

```python
%matplotlib inline
```

# Maximizing equilibrium total offspring

```python
x1, x2, x3, x4 = sym.symbols('x1, x2, x3, x4', real=True, nonnegative=True)
T, R, P, S = sym.symbols('T, R, P, S', real=True, positive=True)
M, m = sym.symbols("M, m", real=True, nonnegative=True)
epsilon = sym.symbols("epsilon", real=True, nonnegative=True)

UGA = symbolics.UGA
UgA = symbolics.UgA
```

```python
x = np.array([[x1], [x2], [x3], [1 - x1 - x2 - x3]])
payoff_kernel = np.array([[R, S], [T, P]])
W = models.generalized_sexual_selection(x, UGA, UgA, payoff_kernel, M, m, epsilon)
N, = models.total_offspring(W, x)
```

```python
sym.factor(sym.cancel(sym.together(sym.expand(N))), UGA(x1 + x3), UgA(x1 + x3), x1, x2)
```

$\displaystyle 4.0 \left(0.5 P - 0.5 m + x_{1} \left(- 0.5 M + 0.5 m\right) + x_{1} \left(- 1.0 P + 0.5 S + 0.5 T\right) \operatorname{UGA}{\left(x_{1} + x_{3} \right)} + x_{1} \left(1.0 P - 0.5 S - 0.5 T\right) \operatorname{UgA}{\left(x_{1} + x_{3} \right)} + x_{1} \left(- 0.5 P - 0.5 R + 0.5 S + 0.5 T\right) \operatorname{UgA}^{2}{\left(x_{1} + x_{3} \right)} + x_{1} \left(0.5 P + 0.5 R - 0.5 S - 0.5 T\right) \operatorname{UGA}^{2}{\left(x_{1} + x_{3} \right)} + x_{2} \left(- 0.5 M + 0.5 m\right) + x_{2} \left(- 1.0 P + 0.5 S + 0.5 T\right) \operatorname{UGA}{\left(x_{1} + x_{3} \right)} + x_{2} \left(1.0 P - 0.5 S - 0.5 T\right) \operatorname{UgA}{\left(x_{1} + x_{3} \right)} + x_{2} \left(- 0.5 P - 0.5 R + 0.5 S + 0.5 T\right) \operatorname{UgA}^{2}{\left(x_{1} + x_{3} \right)} + x_{2} \left(0.5 P + 0.5 R - 0.5 S - 0.5 T\right) \operatorname{UGA}^{2}{\left(x_{1} + x_{3} \right)} + \left(- 1.0 P + 0.5 S + 0.5 T\right) \operatorname{UgA}{\left(x_{1} + x_{3} \right)} + \left(0.5 P + 0.5 R - 0.5 S - 0.5 T\right) \operatorname{UgA}^{2}{\left(x_{1} + x_{3} \right)}\right)$

Total offspring in the two-locus model is a convex combination of total offspring in the one-locus model where all females carry G-allele of the $\gamma$ gene and the one-locus model where all females carry the g-allele of the $\gamma$ gene.

\begin{align}
N(x_G, x_A; U_G, G_g) =& x_G \Bigg(2\bigg(\big((R + P) - (T + S)\big)U_G(x_A)^2 + \big((T + S) - 2P\big)U_G(x_A) + (P - M)\bigg)\Bigg) + (1 - x_G)\Bigg(2\bigg(\big((R + P) - (T + S)\big)U_g(x_A)^2 + \big((T + S) - 2P\big)U_g(x_A) + (P - m)\bigg)\Bigg) \\
=& x_G N(x_A; U_G) + (1 - x_G)N(x_A, U_g)
\end{align}

Note that the function for total offspring is linear in $x_G$. This fact implies that equilibrium total offspring will be maximized at either $x_G*=0$ or $x_G^*=1$, depending on parameters.  Thus any stable, fully polymorphic equilibrium will *not* maximize total offspring in equilibrium.

## Substitute equilibrium values

```python
UGA_star, UgA_star, xG_star = sym.symbols("UGA_star, UgA_star, xG_star")
equilibrium_total_offspring = N.subs({UGA(x1+x3): UGA_star, UgA(x1+x3): UgA_star, x1: xG_star - x2}).simplify()
```

```python
sym.factor(sym.cancel(equilibrium_total_offspring.subs({xG_star: 0, m: 0})), UgA_star)
```

$\displaystyle 4.0 \left(0.5 P + UgA_{star}^{2} \left(0.5 P + 0.5 R - 0.5 S - 0.5 T\right) + UgA_{star} \left(- 1.0 P + 0.5 S + 0.5 T\right)\right)$

$$ N\bigg(\frac{1}{2\epsilon + 1}, 1, Ug\bigg) = \frac{2(R - M) + 4\epsilon\bigg(Ug^2\big((R + P) - (T + S)\big) + Ug\big((T + S) - 2P\big) + P\bigg)}{2\epsilon + 1} $$

## Make an interactive plot

```python
_equilibrium_total_offspring = sym.lambdify((xG_star, UGA_star, UgA_star, T, R, P, S, M, m),
                                            equilibrium_total_offspring,
                                            modules="numpy")
```

```python
def plot_total_offspring(xG_star, T, R, P, S, M):

    fig, ax = plt.subplots(1, 1, figsize=(20, 10))

    equilibrium_selection_probs = np.linspace(0, 1, 100).reshape(-1, 1)
    UGAs = equilibrium_selection_probs.reshape(-1, 1)
    UgAs = equilibrium_selection_probs.reshape(1, -1)
    Z = _equilibrium_total_offspring(xG_star, UGAs, UgAs, T, R, P, S, M, 0)
    cax = ax.imshow(Z, origin="lower")

    contours = ax.contour(Z, colors='w', origin='lower')
    ax.clabel(contours, contours.levels, inline=True, fontsize=10)

    ax.set_ylabel(r"$U_{GA}^*$", fontsize=20, rotation="horizontal")
    ax.set_xlabel(r"$U_{gA}^*$", fontsize=20)
    ax.set_title(r"Equilibrium max total offspring for $x_G^*$={} is {}".format(xG_star, Z.max()), fontsize=25)
    ax.grid(False)

    # adjust the tick labels
    locs, _ = plt.xticks()
    plt.xticks(locs[1:], np.linspace(0, 1, locs.size-1))
    locs, _ = plt.yticks()
    plt.yticks(locs[1:], np.linspace(0, 1, locs.size-1))

    plt.show()
```

```python
xG_slider = widgets.FloatSlider(value=0.5, min=0, max=1, step=0.01, description=r"$x_G^*$")

# sliders used to control the Prisoner's Dilemma Payoffs
T_slider = widgets.FloatSlider(value=10, min=0, max=100, step=0.1, description=r"$T$")
R_slider = widgets.FloatSlider(value=3, min=0, max=100, step=0.1, description=r"$R$")
P_slider = widgets.FloatSlider(value=2, min=0, max=100, step=0.1, description=r"$P$")
S_slider = widgets.FloatSlider(value=1, min=0, max=100, step=0.1, description=r"$S$")

M_slider = widgets.FloatSlider(value=0, min=0, max=100, step=0.1, description=r"$M$")

w = widgets.interactive(plot_total_offspring, xG_star=xG_slider, T=T_slider, R=R_slider, P=P_slider, S=S_slider, M=M_slider)
display(w)
```

    interactive(children=(FloatSlider(value=0.5, description='$x_G^*$', max=1.0, step=0.01), FloatSlider(value=10.…

## Find the optimal values of $x_G^*, U_{GA}^*, U_{gA}^*$.

The number of total offspring can be written as a function of the equilibrium selection probability.

$$ N\big(x_G^*, U_{GA}^*, U_{gA}^*\big) = 2\bigg(\big((R + P) - (T + S)\big)x_G^*U_{GA}^{*2} + \big((T + S) - 2P\big)x_G^*U_{GA}^* + P - \big((R + P) - (T + S)\big)x_G^*U_{gA}^{*2} - \big((T + S) - 2P\big)x_G^*U_{gA}^* + \big((R + P) - (T + S)\big)U_{gA}^{*2} + \big((T + S) - 2P\big)U_{gA}^* \bigg)$$

To find the equilibrium selection probability that maximizes the number of total offspring we need to solve the following constrained optimization problem.

$$ \max_{x_G^*, U_{GA}^*, U_{gA}^*}\ N\big(x_G^*, U_{GA}^*, U_{gA}^*\big)  $$

subject to the following inequality constraints.

\begin{align}
    -x_G^* \le& 0 \\
    x_G^* - 1 \le& 0 \\
    -U_{GA}^* \le& 0 \\
    U_{GA}^* - 1 \le& 0 \\
    -U_{gA}^* \le& 0 \\
    U_{gA}^* - 1 \le& 0
\end{align}

First-order conditions are as follows.
\begin{align}
    2\bigg(\big((R + P) - (T + S)\big)U_{GA}^{*2} + \big((T + S) - 2P\big)U_{GA}^* + P - \big((R + P) - (T + S)\big)U_{gA}^{*2} - \big((T + S) - 2P\big)U_{gA}^*\bigg) =& -\mu_{x_G^*, 0} + \mu_{x_G^*,1} \\
    2\bigg(2\big((R + P) - (T + S)\big)x_G^*U_{GA}^* + \big((T + S) - 2P\big)x_G^*\bigg) =& -\mu_{U_{GA}^*, 0} + \mu_{U_{GA}^*,1} \\
    2\bigg(-2\big((R + P) - (T + S)\big)x_G^*U_{gA}^* - \big((T + S) - 2P\big)x_G^* + 2\big((R + P) - (T + S)\big)U_{gA}^* + \big((T + S) - 2P\big) \bigg) =& -\mu_{U_{gA}^*, 0} + \mu_{U_{gA}^*,1}
\end{align}

Complementary slackness conditions are

\begin{align}
    -\mu_{x_G^*,0}x_G^* =& 0 \\
    \mu_{x_G^*,1}\big(x_G^* - 1\big) =& 0 \\
    -\mu_{U_{GA}^*,0}U_{GA}^* =& 0 \\
    \mu_{U_{GA}^*,1}\big(U_{GA}^* - 1\big) =& 0
    -\mu_{U_{gA}^*,0}U_{gA}^* =& 0 \\
    \mu_{U_{gA}^*,1}\big(U_{gA}^* - 1\big) =& 0
\end{align}

where $\mu_0, \mu_1$ are Lagrange multipliers.

### Case 1: interior equilibrium $(0 < x_G^* < 1, 0 < U_{GA}^* < 1, 0 < U_{gA}^* < 1)$

In an interior equilibrium, complementary slackness conditions imply that all Lagrange multipliers are zero (i.e., $\mu_{x_G^*, 0} =\mu_{x_G^*, 1} =\mu_{U_{GA}^*, 0} =\mu_{U_{GA}^*, 0} =\mu_{U_{gA}^*, 0} =\mu_{U_{gA}^*, 0}=0$).

Our first order conditions reduce to the following.

\begin{align}
    2\bigg(\big((R + P) - (T + S)\big)U_{GA}^{*2} + \big((T + S) - 2P\big)U_{GA}^* + P - \big((R + P) - (T + S)\big)U_{gA}^{*2} - \big((T + S) - 2P\big)U_{gA}^*\bigg) =& 0 \\
    2\bigg(2\big((R + P) - (T + S)\big)x_G^*U_{GA}^* + \big((T + S) - 2P\big)x_G^*\bigg) =& 0 \\
    2\bigg(-2\big((R + P) - (T + S)\big)x_G^*U_{gA}^* - \big((T + S) - 2P\big)x_G^* + 2\big((R + P) - (T + S)\big)U_{gA}^* + \big((T + S) - 2P\big) \bigg) =& 0
\end{align}

Rearranging the second first-order condition yields an expression for the optimal value of $U_{GA}^*$.

$$ \bar{U}_{GA}^* = \frac{1}{2}\left(\frac{2P - (T + S)}{(R + P) - (T + S)}\right) $$

Substituting this result into the first first-order condition and rearranging yields an identical exprssion for the optimal value of $U_{gA}^*$.

$$ \bar{U}_{gA}^* = \frac{1}{2}\left(\frac{2P - (T + S)}{(R + P) - (T + S)}\right) $$

Substituting this result into the third first-order condition yields a result which implies that the optimal value for $x_G^*$ is indeterminate (i.e., the objective is flat when holding $U_{GA}^*$ and $U_{gA}^*$ fixed).

```python
first_order_conditions = sym.Matrix([equilibrium_total_offspring.diff(xG_star, 1),
                                     equilibrium_total_offspring.diff(UGA_star, 1),
                                     equilibrium_total_offspring.diff(UgA_star, 1)])
```

```python
optimal_UGA_star, = sym.solve(first_order_conditions[1,0], UGA_star)
```

```python
optimal_UgA_star, = sym.solve(first_order_conditions[0,0].subs({UGA_star: optimal_UGA_star}), UgA_star)
```

```python
# optimal value for xG_star is indeterminate!
sym.simplify(first_order_conditions[2,0].subs({UGA_star: optimal_UGA_star, UgA_star: optimal_UgA_star}))
```

    0

```python
jacobian = first_order_conditions.jacobian([xG_star, UGA_star, UgA_star])
simplified_jacobian = sym.simplify(jacobian.subs({UGA_star: optimal_UGA_star, UgA_star: optimal_UgA_star}))
e1, e2, e3 = (simplified_jacobian.eigenvals()
                                 .keys())
```

```python
e1
```

    -4*P*xG_star + 4*P - 4*R*xG_star + 4*R + 4*S*xG_star - 4*S + 4*T*xG_star - 4*T

```python
e2
```

    4*P*xG_star + 4*R*xG_star - 4*S*xG_star - 4*T*xG_star

```python
e3
```

    0

Requirement for total offspring to optimal a local maximum at the above values derived above is for the Hessian to be negative semi-definite. This requirement will be satisfied if and only if

$$ R + P < T + S. $$

### Case 2: equilibrium with $\bar{x}_G^*=1$, $(0 < U_{GA}^* < 1, 0 < U_{gA}^* < 1)$

In this equilibrium, complementary slackness conditions imply that all Lagrange multipliers are zero (i.e., $\mu_{x_G^*, 0} =\mu_{U_{GA}^*, 0} =\mu_{U_{GA}^*, 0} =\mu_{U_{gA}^*, 0} =\mu_{U_{gA}^*, 0}=0$) except $\mu_{x_G^*, 1}  > 0$.

Our first order conditions reduce to the following.

\begin{align}
    2\bigg(\big((R + P) - (T + S)\big)U_{GA}^{*2} + \big((T + S) - 2P\big)U_{GA}^* + P - \big((R + P) - (T + S)\big)U_{gA}^{*2} - \big((T + S) - 2P\big)U_{gA}^*\bigg) =& \mu_{x_G^*, 1} \\
    2\bigg(2\big((R + P) - (T + S)\big)U_{GA}^* + \big((T + S) - 2P\big)\bigg) =& 0 \\
    2\bigg(-2\big((R + P) - (T + S)\big)U_{gA}^* - \big((T + S) - 2P\big) + 2\big((R + P) - (T + S)\big)U_{gA}^* + \big((T + S) - 2P\big) \bigg) =& 0
\end{align}

Rearranging the second first-order condition yields an expression for the optimal value of $U_{GA}^*$.

$$ \bar{U}_{GA}^* = \frac{1}{2}\left(\frac{2P - (T + S)}{(R + P) - (T + S)}\right) $$

Substituting this optimal value of $U_{GA}^*$ into the first first-order condition and rearranging we find that the inequality will hold so long as

$$ \big((R + P) - (T + S)\big)\big(U_{gA}^* - \bar{U}_{GA}^*\big)^2 + P > 0 $$

which requires $R + P > T + S$. Finally, rearranging the third first-order condition implies that the optimal value for $U_{gA}^*$ is indeterminate: so long as $R + P > T + S$, then $\bar{x}_G^*=1$ for any value of $U_{gA}^*$.

```python
def interior_optimal_UGA(T, R, P, S):
    return 0.5 * ((2 * P - (T + S)) / ((R + P) - (T + S)))

def interior_optimal_UgA(T, R, P, S):
    return interior_optimal_UGA(T, R, P, S)

def _mu_xG_1(UGA, UgA, T, R, P, S):
    multiplier = 2 * (((R + P) - (T + S)) * UGA**2 + ((T + S) - 2 * P) * UGA + P -
                      ((R + P) - (T + S)) * UgA**2 - ((T + S) - 2 * P) * UgA)
    return multiplier

def max_total_fitness(T, R, P, S):
    if _mu_xG_1(UGA, UgA, T, R, P, S) > 0:
        pass # max at xG=1
    elif _mu_xG_1(UGA, UgA, T, R, P, S) < 0:
        pass # max at xG=0
    else:

```

```python
objective = lambda x: -_equilibrium_total_offspring(x[0], x[1], x[2], 25, 3, 2, 1)
x0 = 0.5 * np.ones(3)
res = optimize.minimize(objective, x0, bounds=[(0,1), (0,1), (0,1)])
```

```python
-res.fun
```

    15.523809523809522

```python

```


# Poission-Gaussian noise

(c) 2019 Manuel Razo. This work is licensed under a [Creative Commons Attribution License CC-BY 4.0](https://creativecommons.org/licenses/by/4.0/). All code contained herein is licensed under an [MIT license](https://opensource.org/licenses/MIT).

---

```python
import os
import pickle
import glob
import cloudpickle
import git
import numpy as np
import pandas as pd

# To compute symbolic expressions
import sympy
sympy.init_printing(use_unicode=True, use_latex=True) # print outputs in LaTeX

# Import matplotlib stuff for plotting
import matplotlib.pyplot as plt
import matplotlib.cm as cm
import matplotlib as mpl
# Seaborn, useful for graphics
import seaborn as sns

# Import the project utils
import ccutils

# This enables high resolution graphics inline
%config InlineBackend.figure_format = 'retina'

# Find home directory for repo
repo = git.Repo("./", search_parent_directories=True)
homedir = repo.working_dir

tmpdir = f'{homedir}/tmp/'
figdir = f'{homedir}/fig/moment_dynamics_numeric/'
datadir = f'{homedir}/data/csv_maxEnt_dist/'
```

```python
# Set PBoC plotting format
ccutils.viz.set_plotting_style()
# Increase dpi
mpl.rcParams['figure.dpi'] = 110
```

### $\LaTeX$ macros
$\newcommand{kpon}{k^{(p)}_{\text{on}}}$
$\newcommand{kpoff}{k^{(p)}_{\text{off}}}$
$\newcommand{kron}{k^{(r)}_{\text{on}}}$
$\newcommand{kroff}{k^{(r)}_{\text{off}}}$
$\newcommand{rm}{r _m}$
$\newcommand{gm}{\gamma _m}$
$\newcommand{ee}[1]{\left\langle #1 \right\rangle}$
$\newcommand{bb}[1]{\mathbf{#1}}$
$\newcommand{th}[1]{{#1}^{\text{th}}}$
$\newcommand{dt}[1]{{\partial{#1} \over \partial t}}$
$\newcommand{Km}{\bb{K}}$
$\newcommand{Rm}{\bb{R}_m}$
$\newcommand{Re}{\bb{R}_m'}$
$\newcommand{Gm}{\bb{\Gamma}_m}$
$\newcommand{Rp}{\bb{R}_p}$
$\newcommand{Gp}{\bb{\Gamma}_p}$

## Poisson-Gaussian noise for microscopy

In this notebook we will explore a probabilistic model to account for the experimental noise in the images we obtained with fluorescent microscopy. Specifically we will explore a mixture model known as the Poission-Gaussian model. This is a common model in the literature used to account for three types of noise:
1. Dark noise: Electronic noise in the reader due to thermal noise.
2. Shot noise: Randomness in the number of detected photons sensed at a given exposure level.
3. Readout noise: Noise generated during the conversion of charges into voltages.
In this model a readout $\nu$ is given by
$$
\nu = \alpha x + n,
$$
where $x \sim \text{Poisson}(\lambda)$ is a Poisson random variable, $\alpha$ is a gain constant that sets how much the shot noise affects the measurements, and $n \sim \mathcal{N}(0, \sigma^2)$ is a normally distributed random variable that accounts for the readout noise. In this sense a given readout $\nu$ has a probability
$$
P(\nu) = \sum_{x=0}^\infty P(\nu \mid x) P(x),
$$
where $P(x)$ is the Poisson probability mass function, i.e.
$$
P(x) = {\lambda^x e^{-\lambda} \over x!},
$$
and $P(\nu \mid x)$ is the Gaussian probability density function, i.e.
$$
P(\nu \mid x) = {1 \over \sqrt{2 \pi \sigma^2}}
                e^{-{(\nu - \alpha x)^2 \over 2 \sigma^2}}.
$$

Putting this together then results in
$$
P(\nu) = \sum_{x=0}^\infty
    {\lambda^x e^{-\lambda} \over x!}
    {1 \over \sqrt{2 \pi \sigma^2}}
                e^{-{(\nu - \alpha x)^2 \over 2 \sigma^2}}.
$$

### Applying this noise model to single-cell reads.

For our particular case in which we determine the intensity of a single-cell we will assume that the intensity we measure $\nu$ depends on the number of proteins in the cell, such that for a given readout we have
$$
P(\nu) = \sum_{p = 0}^\infty P(\nu \mid p)P(p),
$$
where we take the sum over all possible protein copy numbers. If we include the Poisson-Gaussian noise we just covered, then this becomes
$$
P(\nu) = \sum_{p = 0}^\infty \sum_{x=0}^\infty
         P(\nu \mid x, p) P(x \mid p) P(p),
$$
where we now condition the shot noise on the number of proteins $P(x \mid p)$.

## Computing the moments of the Poission-Gaussian distribution

The objective of this notebook is to systematically compute the moment sof the distribution using `sympy`'s engine in order to handle the messy algebra. At the end of the day we want to explore if this model can explain the discrepancy that we see at the level of the noise (std/mean) and skewness of gene expression.

We will not ask `sympy` to compute the sums and obtain the moments themselves since that would take a lot of effort. We will simply use our knowledge of the distribution moments and just use sympy to simplify things.

Let's define the necessary variables

```python
# Define variables
nu, x, = sympy.symbols('\\nu x')
# Define parameters
alpha, sigma_2, lam = sympy.symbols('\\alpha \\sigma^2 \\lambda')
# Define protein moments
p1 = sympy.Symbol('\\left\\langle p \\right\\rangle')
p2 = sympy.Symbol('\\left\\langle p^2 \\right\\rangle')
p3 = sympy.Symbol('\\left\\langle p^3 \\right\\rangle')

n1 = sympy.Symbol('\\left\\langle \\nu \\right\\rangle')
n2 = sympy.Symbol('\\left\\langle \\nu^2 \\right\\rangle')
n3 = sympy.Symbol('\\left\\langle \\nu^3 \\right\\rangle')

nu, x, alpha, sigma_2, lam, p1, p2, p3, n1, n2, n3
```

#### Mean $\ee{\nu}$

To compute the first moment we have
$$
\ee{\nu} = \int_{-\infty}^{\infty} d\nu \; \nu P(\nu).
$$
Substituting $P(\nu)$ gives
$$
\ee{\nu} =
\int_{-\infty}^{\infty} d\nu \; \nu
\sum_{p = 0}^\infty \sum_{x=0}^\infty
P(\nu \mid x, p) P(x \mid p) P(p).
$$
By rearranging the sums and the integral we can write
$$
\ee{\nu} =
\sum_{p = 0}^\infty P(p)
\sum_{x = 0}^\infty P(x \mid p)
\int_{-\infty}^{\infty} d\nu \; \nu P(\nu \mid x, p).
$$
The term inside the integral is the expected value $\ee{\nu \mid x, p}$ which given our definition of this conditional distribution is the mean
$$
\int_{-\infty}^{\infty} d\nu \; \nu P(\nu \mid x, p) = \alpha x.
$$

Substituting this result gives
$$
\ee{\nu} =
\sum_{p = 0}^\infty P(p)
\sum_{x = 0}^\infty P(x \mid p)
\; \alpha x
$$
The internal sum is again the expected value $\ee{x \mid p} = \lambda p$ given that we have $p$ proteins that emit on average $\lambda$ photons. Using again this result gives
$$
\ee{\nu} =
\sum_{p = 0}^\infty P(p)
\; \alpha \lambda p.
$$
Finally this sum is just the expected value of the number of proteins. Therefore we have that the mean intensity $\ee{\nu}$ is of the form
$$
\ee{\nu} = \alpha \lambda \ee{p}.
$$

Let's define this mean in `sympy`.

```python
nu_first = alpha * lam * p1
sympy.Eq(n1, nu_first)
```

#### Second moment $\ee{\nu^2}$

For the second moment we can follow a similar procedure. First we define the integral we need to compute
$$
\ee{\nu^2} = \int_{-\infty}^{\infty} d\nu \; \nu^2 P(\nu).
$$
Substituting $P(\nu)$ gives
$$
\ee{\nu^2} =
\int_{-\infty}^{\infty} d\nu \; \nu^2
\sum_{p = 0}^\infty \sum_{x=0}^\infty
P(\nu \mid x, p) P(x \mid p) P(p).
$$
By rearranging the sums and the integral we can write
$$
\ee{\nu^2} =
\sum_{p = 0}^\infty P(p)
\sum_{x = 0}^\infty P(x \mid p)
\int_{-\infty}^{\infty} d\nu \; \nu^2 P(\nu \mid x, p).
$$
The term inside the integral is the expected value $\ee{\nu^2 \mid x, p}$ which given our definition of this conditional distribution is the variance + mean squared,
$$
\int_{-\infty}^{\infty} d\nu \; \nu^2 P(\nu \mid x, p) =
\sigma^2 + (\alpha x)^2.
$$

Substituting this result gives
$$
\ee{\nu^2} =
\sum_{p = 0}^\infty P(p)
\sum_{x = 0}^\infty P(x \mid p)
\; \sigma^2 + \alpha^2 x^2
$$
Distributing the internal sum results in
$$
\ee{\nu^2} =
\sum_{p = 0}^\infty P(p)
\left[
\sigma^2 \sum_{x=0}^\infty P(x \mid p) +
\alpha^2 \sum_{x=0}^\infty x^2 P(x \mid p)
\right].
$$
The first sum inside the brackets is just the normalization, i.e.
$$
\sum_{x=0}^\infty P(x \mid p) = 1.
$$
The second sum is the 2nd moment of a Poisson distribution. This can be sown to be equal to $(\lambda p)^2 + \lambda p$. So we have
$$
\sum_{x=0}^\infty x^2 P(x \mid p) = (\lambda p)^2 + \lambda p.
$$
Substituting these results gives
$$
\ee{\nu^2} =
\sum_{p = 0}^\infty P(p)
\left[
\sigma^2 +
\alpha^2 \lambda^2 p^2 + \alpha^2 \lambda p
\right].
$$

Taking the sum over all values of $p$ results in
$$
\ee{\nu^2} = \sigma^2 +
\alpha^2 \lambda^2 \ee{p^2} +
\alpha^2 \lambda \ee{p}.
$$
Let's define this in sympy.

```python
# Define second moment
nu_second = sigma_2 + alpha**2 * lam**2 * p2 + alpha**2 * lam * p1
sympy.Eq(n2, nu_second)
```

The variance is then given by

```python
# Compute variance
nu_var = nu_second - nu_first**2
sympy.Eq(n2 - n1**2, nu_var.simplify())
```

And the noise squared (var / mean$^2$) takes the form

```python
nu_noise_2 = nu_var / nu_first**2
sympy.Eq((n2 - n1**2) / n1**2, nu_noise_2.simplify())
```

For our expected range of $\lambda$ and $\ee{p}$ we can assume that the third term in the sum can be neglected. This then results

```python
args = nu_noise_2.simplify().args
args = [x for i, x in enumerate(args) if i != 2]
nu_noise_approx = sum(args)
sympy.Eq((n2 - n1**2) / n1**2, nu_noise_approx)
```

Experimentally we have determined that this noise is 3 times larger than what the theory predicts. Let's solve for the model parameters given this condition. First we set the equation.

```python
# Define theoretical noise
p_noise_2 = (p2 - p1**2) / p1**2

# Set equation
noise_eq = sympy.Eq(nu_noise_approx, 4 * p_noise_2)
noise_eq
```

Now let's try to solve for the parameters $\sigma^2 / \alpha^2 \lambda^2$

```python
# Solve equation for parameters
noise_sol = sympy.solve(noise_eq, sigma_2 / alpha**2 / lam**2)
sympy.Eq(sigma_2 / (alpha**2 * lam**2), noise_sol[0])
```

#### Third moment $\ee{\nu^3}$

The third moment is the really tricky one algebraically. first just as with the previous moments we begin defining the integral to be computed.
$$
\ee{\nu^3} = \int_{-\infty}^{\infty} d\nu \; \nu^3 P(\nu).
$$
Substituting $P(\nu)$ gives
$$
\ee{\nu^3} =
\int_{-\infty}^{\infty} d\nu \; \nu^3
\sum_{p = 0}^\infty \sum_{x=0}^\infty
P(\nu \mid x, p) P(x \mid p) P(p).
$$
By rearranging the sums and the integral we can write
$$
\ee{\nu^3} =
\sum_{p = 0}^\infty P(p)
\sum_{x = 0}^\infty P(x \mid p)
\int_{-\infty}^{\infty} d\nu \; \nu^3 P(\nu \mid x, p).
$$
The term inside the integral is the expected value $\ee{\nu^3 \mid x, p}$ which given our definition of this conditional distribution is of the form
$$
\int_{-\infty}^{\infty} d\nu \; \nu^3 P(\nu \mid x, p) =
(\alpha x)^3 + 3 (\alpha x) \sigma^2,
$$
i.e. the third moment of a Gaussian distribution.

Substituting this result gives
$$
\ee{\nu^3} =
\sum_{p = 0}^\infty P(p)
\sum_{x = 0}^\infty P(x \mid p)
\left[
\; (\alpha x)^3 + 3 (\alpha x) \sigma^2
\right]
$$
Distributing the internal sum results in
$$
\ee{\nu^3} =
\sum_{p = 0}^\infty P(p)
\left[
\alpha^3 \sum_{x=0}^\infty x^3 P(x \mid p) +
3 \alpha \sigma^2 \sum_{x=0}^\infty x P(x \mid p)
\right].
$$
The first sum inside the brackets is the third moment of a Poisson distribution given by
$$
\sum_{x=0}^\infty x^3 P(x \mid p) =
(\lambda p)^3 + 3(\lambda p)^2 + (\lambda p).
$$
The second sum is the mean of a Poisson distribution. So we have
$$
\sum_{x=0}^\infty x P(x \mid p) = \lambda p.
$$
Substituting these results gives
$$
\ee{\nu^3} =
\sum_{p = 0}^\infty P(p)
\left[
\alpha^3 \left( (\lambda p)^3 + 3(\lambda p)^2 + (\lambda p) \right) +
3 \alpha \sigma^2 \left( \lambda p \right)
\right].
$$
Finally we take the sum over all values of $p$, obtaining
$$
\ee{\nu^3} =
\alpha^3
\left(
\lambda^3 \ee{p^3} +
3 \lambda^2 \ee{p}^2 + \lambda \ee{p}
\right) +
3 \alpha \sigma^2 \lambda \ee{p}.
$$
Let's define this in sympy.

```python
nu_third = alpha**3 * (lam**3 * p3 + 3 * lam**2 * p2 + lam * p1) +\
           3 * alpha * sigma_2 * lam * p1
sympy.Eq(n3, nu_third)
```

With this in hand we can now compute the skewness of the distribution. The skewness $S(\nu)$ defined in terms of other moments is given by
$$
S(\nu) = {\ee{\nu ^3} - 3 \ee{\nu} \text{Var}(\nu) - \ee{\nu}^3 \over
          \text{Var}(\nu)^{3/2}}.
$$
Let's now compute this with `sympy`. This is the real reason we used this notebook because the algebra can get really messy.

```python
# Define skewness
nu_skew = (nu_third - 3 * nu_first * nu_var - nu_first**3) / \
          nu_var**(sympy.Rational(3, 2))
nu_skew
```

Again by experimental evidence we know that this skewness is twice as large as the theoretical prediction. Let's see if we can infer anything from this.

```python
# Define protein skewness
p_skew = (p3 - 3 * p1 * (p2 - p1**2) - p1**3) /\
         (p2 - p1**2)**(sympy.Rational(3, 2))

skew_eq = sympy.Eq(nu_skew, 2 * p_skew)
skew_eq.simplify()
```

There is nothing crystal clear here. Let's try a different approach

## "Experimental mathematics" with moments

From the realm of what we would call experimental mathematics we can see that our numerical integrations of the moment equations have an interesting relationship between the moments of the protein distribution. Specifically we can express both the second moment $\ee{p^2}$ and the third moment $\ee{p^3}$ as simple functions of the first moment. Specifically we can show that to a very good approximation
$$
\ee{p^2} \approx \ee{p}^2 + m_{21},
$$
where $m_{21}$ is an intercept done when performing a linear regression between $\ee{p}^2$ and $\ee{p^2}$. The slope of this regression is really close to 1, that's why we approximate it to be 1. The same is true when performing a linear regression between $\ee{p}^3$ and $\ee{p^3}$, so we approximate this moment as
$$
\ee{p^3} \approx \ee{p}^3 + m_{31},
$$

Using these substitutions let's see if we find something interesting. First let's look at the noise. For this we will substitute our previous results with these approximations for the moments.

```python
# Define linear regression variables
m21, m31 = sympy.symbols('m_{21} m_{31}')

nu_noise_subs = nu_noise_approx.subs(
    {p2: p1**2 + m21, p3: p1**3 + m31}
)
p_noise_subs = p_noise_2.subs(
    {p2: p1**2 + m21, p3: p1**3 + m31}
)

noise_eq = sympy.Eq(nu_noise_subs, 4 * p_noise_subs)
noise_eq.simplify()
```

Now we can solve for the parameters of our Poisson-Gaussian model

```python
sympy.Eq(
    sigma_2 / alpha**2 / lam**2,
    sympy.solve(noise_eq, sigma_2 / alpha**2 / lam**2)[0]
        )
```

This is quite interesting. What this says is that in order to account for the factor of 4 difference in our noise squared (var / mean$^2$), a constant set of parameters equivalent to 3 times the intercept that relates $\ee{p^2}$ and $\ee{p}$ would solve the issue.

Let's repeat this with the skewness.

```python
# Define factor between experimental and theoretical skewness as variable
n = sympy.Symbol('n')
# Substitute approximation of moments
nu_skew_subs =  nu_skew.subs({p2: p1**2 + m21, p3: p1**3 + m31})
p_skew_subs = p_skew.subs({p2: p1**2 + m21, p3: p1**3 + m31})

skew_eq = sympy.Eq(nu_skew_subs, n * p_skew_subs)

skew_eq
```

Now let's substitute $\sigma^2$ as a function of the other parameters.

```python
skew_eq_subs = skew_eq.subs(
    {sigma_2 : 3 * m21 * alpha**2 * lam**2}
).simplify()
skew_eq_subs
```

Now let's see if we can solve for $n$ and obtain what we would expect.

```python
sympy.Eq(n, sympy.solve(skew_eq_subs, n)[0])
```

Not really. This is a function of $\ee{p}$


```python
from IPython.display import display, Markdown, Latex
import os
import os.path
import pandas
import functools

from ee import EeVal
from ee.formatting import e12, e24
#from ee.kicad.bom.io import read_bom
from ee.kicad import read_schematic, read_schematics, to_pandas

%load_ext autoreload
%autoreload 2

```

    The autoreload extension is already loaded. To reload it, use:
      %reload_ext autoreload

```python
bom_file = "alpha.sch"
components = to_pandas(read_schematics(bom_file))
```

### LED resistors

```python
leds = components[components.role == "LED"]

def resistance(led):
    i_target = EeVal(led.req_I).value
    v_resistor = EeVal(led.gpio_voltage).value - EeVal(led.req_Vf).value
    r_exact = v_resistor/i_target
    r = e12.closest(r_exact)
    i_exact = EeVal("{} A".format(v_resistor/r))
    return pandas.Series({"r": r, "r_exact": r_exact, "i": i_exact})

resistances = leds.apply(resistance, axis='columns', reduce=False)

for ref, led in leds.join(resistances).iterrows():
    display(Markdown("#### LED {}".format(ref)))
    display(Latex("""
$$
\\begin{{align}}
V_{{forward}} &= {} \\\\
I             &= {} \\\\
R_{{exact}}   &= {} \Omega \\\\
\\\\
R_{{e12}}     &= {} \Omega \\\\
I_{{LED}}     &= {} A
\\end{{align}}
$$
""".format(led.req_Vf, led.req_I, led.r_exact, led.r, led.i)))
```

#### LED D2

$$
\begin{align}
V_{forward} &= 1.9V \\
I             &= 5 mA \\
R_{exact}   &= 280.0 \Omega \\
\\
R_{e12}     &= 270.0 \Omega \\
I_{LED}     &= 5.19 m A
\end{align}
$$

#### LED D4

$$
\begin{align}
V_{forward} &= 1.9V \\
I             &= 5 mA \\
R_{exact}   &= 280.0 \Omega \\
\\
R_{e12}     &= 270.0 \Omega \\
I_{LED}     &= 5.19 m A
\end{align}
$$

```python
components = components.join(resistances)
```


## Spatial Estimation with Inverse Distance

### Michael Pyrcz, Associate Professor, University of Texas at Austin

##### [Twitter](https://twitter.com/geostatsguy) | [GitHub](https://github.com/GeostatsGuy) | [Website](http://michaelpyrcz.com) | [GoogleScholar](https://scholar.google.com/citations?user=QVZ20eQAAAAJ&hl=en&oi=ao) | [Book](https://www.amazon.com/Geostatistical-Reservoir-Modeling-Michael-Pyrcz/dp/0199731446) | [YouTube](https://www.youtube.com/channel/UCLqEr-xV-ceHdXXXrTId5ig)  | [LinkedIn](https://www.linkedin.com/in/michael-pyrcz-61a648a1)

#### Spatial Estimation

Consider the case of making an estimate at some unsampled location, $𝑧(\bf{u}_0)$, where $z$ is the property of interest (e.g. porosity etc.) and $𝐮_0$ is a location vector describing the unsampled location.

How would you do this given data, $𝑧(\bf{𝐮}_1)$, $𝑧(\bf{𝐮}_2)$, and $𝑧(\bf{𝐮}_3)$?

It would be natural to use a set of linear weights to formulate the estimator given the available data.

\begin{equation}
z^{*}(\bf{u}) = \sum^{n}_{\alpha = 1} \lambda_{\alpha} z(\bf{u}_{\alpha})
\end{equation}

We could add an unbiasedness constraint to impose the sum of the weights equal to one.  What we will do is assign the remainder of the weight (one minus the sum of weights) to the global average; therefore, if we have no informative data we will estimate with the global average of the property of interest.

\begin{equation}
z^{*}(\bf{u}) = \sum^{n}_{\alpha = 1} \lambda_{\alpha} z(\bf{u}_{\alpha}) + \left(1-\sum^{n}_{\alpha = 1} \lambda_{\alpha} \right) \overline{z}
\end{equation}

We will make a stationarity assumption, so let's assume that we are working with residuals, $y$.

\begin{equation}
y^{*}(\bf{u}) = z^{*}(\bf{u}) - \overline{z}(\bf{u})
\end{equation}

If we substitute this form into our estimator the estimator simplifies, since the mean of the residual is zero.

\begin{equation}
y^{*}(\bf{u}) = \sum^{n}_{\alpha = 1} \lambda_{\alpha} y(\bf{u}_{\alpha})
\end{equation}

while satisfying the unbaisedness constraint.

#### Inverse Distance

Now the next question is what weights should we use?

We could use inverse distance weighting, $\lambda = \frac{1}{(d_{0,\alpha})^p} \forall \alpha = 1,\ldots,n$, where $0$ is the unknown location and there are $n$ local data available to support the estimate.

#### Getting Started

Here's the steps to get setup in Python with the GeostatsPy package:

1. Install Anaconda 3 on your machine (https://www.anaconda.com/download/).
2. From Anaconda Navigator (within Anaconda3 group), go to the environment tab, click on base (root) green arrow and open a terminal.
3. In the terminal type: pip install geostatspy.
4. Open Jupyter and in the top block get started by copy and pasting the code block below from this Jupyter Notebook to start using the geostatspy functionality.

You will need to copy the data file to your working directory.  They are available here:

* Tabular data - sample_data_MV_biased.csv available at https://git.io/fhgu0.

There are exampled below with these functions. You can go here to see a list of the available functions, https://git.io/fh4eX, other example workflows and source code.

#### Load the required libraries

The following code loads the required libraries.

```python
import geostatspy.GSLIB as GSLIB                       # GSLIB utilies, visualization and wrapper
import geostatspy.geostats as geostats                 # GSLIB methods convert to Python
```

We will also need some standard packages. These should have been installed with Anaconda 3.

```python
import os                                               # to set current working directory
import numpy as np                                      # arrays and matrix math
import pandas as pd                                     # DataFrames
import matplotlib.pyplot as plt                         # plotting
```

If you get a package import error, you may have to first install some of these packages. This can usually be accomplished by opening up a command window on Windows and then typing 'python -m pip install [package-name]'. More assistance is available with the respective package docs.

#### Set the working directory

I always like to do this so I don't lose files and to simplify subsequent read and writes (avoid including the full address each time).  Also, in this case make sure to place the required (see above) GSLIB executables in this directory or a location identified in the environmental variable *Path*.

```python
os.chdir("d:/PGE383")                                   # set the working directory
```

#### Loading Tabular Data

Here's the command to load our comma delimited data file in to a Pandas' DataFrame object.

```python
df = pd.read_csv("sample_data_MV_biased.csv")                     # read a .csv file in as a DataFrame
df.head()                                               # we could also use this command for a table preview
```

      Unnamed: 0
      X
      Y
      Facies
      Porosity
      Perm
      AI

      0
      0
      100.0
      900.0
      0.0
      0.101319
      1.996868
      5590.417154

      1
      1
      100.0
      800.0
      1.0
      0.147676
      10.711789
      3470.845666

      2
      2
      100.0
      700.0
      1.0
      0.145912
      17.818143
      3586.988513

      3
      3
      100.0
      600.0
      1.0
      0.186167
      217.109365
      3732.114787

      4
      4
      100.0
      500.0
      1.0
      0.146088
      16.717367
      2534.551236

Let's look at summary statistics for all facies combined:

```python
df.describe().transpose()                          # summary table of all facies combined DataFrame statistics
```

      count
      mean
      std
      min
      25%
      50%
      75%
      max

      Unnamed: 0
      368.0
      293.260870
      169.058258
      0.000000
      150.500000
      296.000000
      439.500000
      586.000000

      X
      368.0
      499.565217
      289.770794
      0.000000
      240.000000
      500.000000
      762.500000
      990.000000

      Y
      368.0
      520.644022
      277.412187
      9.000000
      269.000000
      539.000000
      769.000000
      999.000000

      Facies
      368.0
      0.597826
      0.491004
      0.000000
      0.000000
      1.000000
      1.000000
      1.000000

      Porosity
      368.0
      0.127026
      0.030642
      0.041122
      0.103412
      0.125842
      0.148623
      0.210258

      Perm
      368.0
      85.617362
      228.362654
      0.094627
      2.297348
      10.377292
      50.581288
      1991.097723

      AI
      368.0
      4791.736646
      974.560569
      1981.177309
      4110.728374
      4713.325533
      5464.043562
      7561.250336

Let's also look at the distributions.

```python
plt.subplot(111)                                        # plot original sand and shale porosity histograms
plt.hist(df['Porosity'], facecolor='red',bins=np.linspace(0.0,0.25,1000),histtype="stepfilled",alpha=0.2,density=True,cumulative=True,edgecolor='black',label='Original')
plt.xlim([0.05,0.25]); plt.ylim([0,1.0])
plt.xlabel('Porosity (fraction)'); plt.ylabel('Frequency'); plt.title('Porosity')
plt.legend(loc='upper left')
plt.grid(True)

plt.subplots_adjust(left=0.0, bottom=0.0, right=1.0, top=1.2, wspace=0.2, hspace=0.3)
plt.show()
```

We can observe from the CDFs that the porosity distribution is 'Gaussian-like' in shape, while the permeability distribution is 'lognormal-like'.  They both look well behaved.

For brevity we will omit data declustering from this workflow. We will assume declustered means for the porosity and permeability to apply with simple kriging.

Let's plot the location maps of porosity and permeability for all facies. We will also include a cross plot of permeability vs. porosity colored by facies to aid with comparison in spatial features between the porosity and permeability data.

```python
cmap = plt.cm.plasma                    # color map
plt.subplot(111)
GSLIB.locmap_st(df,'X','Y','Porosity',0,1000,0,1000,0,0.25,'Porosity - All Facies','X (m)','Y (m)','Nscore Porosity',cmap)

plt.subplots_adjust(left=0.0, bottom=0.0, right=1.0, top=1.0, wspace=0.5, hspace=0.3)
plt.show()
```

#### Inverse Distance

To demonstrate inverse distance, we include a inverse distance function.

* it is a modification of the 2D kriging program in GeostatsPy

```python
import scipy.spatial as sp  # for fast nearest neighbor search

def invdist(
    df,
    xcol,
    ycol,
    vcol,
    tmin,
    tmax,
    nx,
    xmn,
    xsiz,
    ny,
    ymn,
    ysiz,
    ndmin,
    ndmax,
    radius,
    power
):
    """ Inverse Distance to Python by Michael Pyrcz, the University of Texas at
    Austin (April, 2020).  Based on modification of the GSLIB kb2d program by Deutsch and Journel (1997)
    :param df: pandas DataFrame with the spatial data
    :param xcol: name of the x coordinate column
    :param ycol: name of the y coordinate column
    :param vcol: name of the property column
    :param tmin: property trimming limit
    :param tmax: property trimming limit
    :param nx: definition of the grid system (x axis)
    :param xmn: definition of the grid system (x axis)
    :param xsiz: definition of the grid system (x axis)
    :param ny: definition of the grid system (y axis)
    :param ymn: definition of the grid system (y axis)
    :param ysiz: definition of the grid system (y axis)
    :param ndmin: minimum number of data points to use for kriging a block
    :param ndmax: maximum number of data points to use for kriging a block
    :param radius: maximum isotropic search radius
    :param power: the inverse distance power
    :return:
    """

# Constants
    UNEST = -999.
    EPSLON = 1.0e-10
    VERSION = 0.1

# Load the data
    df_extract = df.loc[(df[vcol] >= tmin) & (df[vcol] <= tmax)]    # trim values outside tmin and tmax
    nd = len(df_extract)
    ndmax = min(ndmax,nd)
    x = df_extract[xcol].values
    y = df_extract[ycol].values
    vr = df_extract[vcol].values

# Allocate the needed memory:
    xa = np.zeros(ndmax)
    ya = np.zeros(ndmax)
    vra = np.zeros(ndmax)
    dist = np.zeros(ndmax)
    nums = np.zeros(ndmax)
    s = np.zeros(ndmax)
    estmap = np.zeros((nx,ny))

# Make a KDTree for fast search of nearest neighbours
    dp = list((y[i], x[i]) for i in range(0,nd))
    data_locs = np.column_stack((y,x))
    tree = sp.cKDTree(data_locs, leafsize=16, compact_nodes=True, copy_data=False, balanced_tree=True)

# Summary statistics for the data after trimming
    avg = vr.mean()
    stdev = vr.std()
    ss = stdev**2.0
    vrmin = vr.min()
    vrmax = vr.max()

# Initialize accumulators:
    rad2 = radius*radius

# MAIN LOOP OVER ALL THE BLOCKS IN THE GRID:
    nk = 0
    ak = 0.0
    vk = 0.0
    for iy in range(0,ny):
        yloc = ymn + (iy-0)*ysiz
        for ix in range(0,nx):
            xloc = xmn + (ix-0)*xsiz
            current_node = (yloc,xloc)

# Find the nearest samples within each octant: First initialize
# the counter arrays:
            na = -1   # accounting for 0 as first index
            dist.fill(1.0e+20)
            nums.fill(-1)
            dist, nums = tree.query(current_node,ndmax) # use kd tree for fast nearest data search
            # remove any data outside search radius
            nums = nums[dist<radius]
            dist = dist[dist<radius]
            nd = len(dist)

# Is there enough samples?
            if nd < ndmin:   # accounting for min index of 0
                est  = UNEST
#                print('UNEST at ' + str(ix) + ',' + str(iy))  # option to include this error
            else:

# Put coordinates and values of neighborhood samples into xa,ya,vra:
                for ia in range(0,nd):
                    jj = int(nums[ia])
                    xa[ia]  = x[jj]
                    ya[ia]  = y[jj]
                    vra[ia] = vr[jj]

# Solve for weights
                dist = np.sqrt((xa-xloc)*(xa-xloc) + (ya-yloc)*(ya-yloc))
                s = 1/((dist + EPSLON)**power)        # calculate inverse weights
                s = s / np.sum(s)             # constrain sum of the weights to 1.0 for unbiasedness
                est = 0.0
                for ia in range(0,nd):
                    est = est + s[ia] * vra[ia]

            estmap[ny-iy-1,ix] = est

# Track the estimates
            if est > UNEST:
                nk = nk + 1
                ak = ak + est
                vk = vk + est*est

# END OF MAIN LOOP OVER ALL THE BLOCKS:

    if nk >= 1:
        ak = ak / float(nk)
        vk = vk/ float(nk) - ak*ak
        print('  Estimated   ' + str(nk) + ' blocks ')
        print('      average   ' + str(ak) + '  variance  ' + str(vk))

    return estmap

```

#### Simple Test at Single Location

Let's test the method for a single estimation location.

* the result was validated with by-hand calculation for inside range, outside range and collocated with a data point

```python
x = [100,600,800]; y = [100,500,700]; vr = [1.0,2.0,3.0]
dftest1 = pd.DataFrame({'X':x,'Y':y,'Var':vr})
tmin = -9999.9; tmax = 9999.9;
nx = 1; xsiz = 10.0; xmn = 500
ny = 1; ysiz = 10.0; ymn = 500
ndmin = 1; ndmax = 100
radius = 1000.0; power = 2

invdist_est = invdist(dftest1,'X','Y','Var',tmin,tmax,nx,xmn,xsiz,ny,ymn,ysiz,ndmin,ndmax,radius,power)
print('The single estimate at x = ' + str(xmn) + ', y = ' + str(ymn) + ' is ' + str(invdist_est[0][0]) + '.')
```

      Estimated   1 blocks
          average   2.041214750542347  variance  0.0
    The single estimate at x = 500, y = 500 is 2.041214750542347.

#### Build a Estimation Map

Now we check the method by building maps with 3 different powers

```python
x = [250,600,800,400]; y = [800,500,700,100]; vr = [1.0,2.0,3.0,4.0]
dftest2 = pd.DataFrame({'X':x,'Y':y,'Var':vr})
tmin = -9999.9; tmax = 9999.9;
nx = 100; xsiz = 10.0; xmn = 5.0; xmin = xmn - 0.5 * xsiz; xmax = xmin + nx * xsiz
ny = 100; ysiz = 10.0; ymn = 5.0; ymin = ymn - 0.5 * ysiz; ymax = ymin + ny * ysiz
ndmin = 1; ndmax = 100
radius = 10000.0; power = 2

invdist_map_power1 = invdist(dftest2,'X','Y','Var',tmin,tmax,nx,xmn,xsiz,ny,ymn,ysiz,ndmin,ndmax,radius,power=1)
invdist_map_power2 = invdist(dftest2,'X','Y','Var',tmin,tmax,nx,xmn,xsiz,ny,ymn,ysiz,ndmin,ndmax,radius,power=2)
invdist_map_power3 = invdist(dftest2,'X','Y','Var',tmin,tmax,nx,xmn,xsiz,ny,ymn,ysiz,ndmin,ndmax,radius,power=3)

# plot the reuslting estimated maps
plt.subplot(131)
GSLIB.locpix_st(invdist_map_power1,xmin,xmax,ymin,ymax,xsiz,0.0,4.0,dftest2,'X','Y','Var','Power = 1','X(m)','Y(m)','Feature',cmap)

plt.subplot(132)
GSLIB.locpix_st(invdist_map_power2,xmin,xmax,ymin,ymax,xsiz,0.0,4.0,dftest2,'X','Y','Var','Power = 2','X(m)','Y(m)','Feature',cmap)

plt.subplot(133)
GSLIB.locpix_st(invdist_map_power3,xmin,xmax,ymin,ymax,xsiz,0.0,4.0,dftest2,'X','Y','Var','Power = 3','X(m)','Y(m)','Feature',cmap)

plt.subplots_adjust(left=0.0, bottom=0.0, right=2.0, top=0.6, wspace=0.2, hspace=0.2)
plt.show()
```

#### Check the Impact of Radius

Radius is the maximum distance to look for neighbouring data

* limited radius may result is image artifacts as data abruptly are excluded

```python
x = [250,600,800,400]; y = [800,500,700,100]; vr = [1.0,2.0,3.0,4.0]
dftest3 = pd.DataFrame({'X':x,'Y':y,'Var':vr})
tmin = -9999.9; tmax = 9999.9;
nx = 100; xsiz = 10.0; xmn = 5.0; xmin = xmn - 0.5 * xsiz; xmax = xmin + nx * xsiz
ny = 100; ysiz = 10.0; ymn = 5.0; ymin = ymn - 0.5 * ysiz; ymax = ymin + ny * ysiz
ndmin = 1; ndmax = 100
power = 2

invdist_map_radius1 = invdist(dftest3,'X','Y','Var',tmin,tmax,nx,xmn,xsiz,ny,ymn,ysiz,ndmin,ndmax,radius = 100,power=2)
invdist_map_radius2 = invdist(dftest3,'X','Y','Var',tmin,tmax,nx,xmn,xsiz,ny,ymn,ysiz,ndmin,ndmax,radius = 500,power=2)
invdist_map_radius3 = invdist(dftest3,'X','Y','Var',tmin,tmax,nx,xmn,xsiz,ny,ymn,ysiz,ndmin,ndmax,radius = 2000,power=2)

# plot the reuslting estimated maps
plt.subplot(131)
GSLIB.locpix_st(invdist_map_radius1,xmin,xmax,ymin,ymax,xsiz,0.0,4.0,dftest3,'X','Y','Var','Radius = 100','X(m)','Y(m)','Feature',cmap)

plt.subplot(132)
GSLIB.locpix_st(invdist_map_radius2,xmin,xmax,ymin,ymax,xsiz,0.0,4.0,dftest3,'X','Y','Var','Radius = 500','X(m)','Y(m)','Feature',cmap)

plt.subplot(133)
GSLIB.locpix_st(invdist_map_radius3,xmin,xmax,ymin,ymax,xsiz,0.0,4.0,dftest3,'X','Y','Var','Radius = 2000','X(m)','Y(m)','Feature',cmap)

plt.subplots_adjust(left=0.0, bottom=0.0, right=2.0, top=0.6, wspace=0.2, hspace=0.2)
plt.show()
```

#### Inverse Distance Porosity Map

Now we are ready to build a map of our porosity spatial feature.

* try experimenting with various powers and radii

```python
tmin = -9999.9; tmax = 9999.9;
nx = 100; xsiz = 10.0; xmn = 5.0; xmin = xmn - 0.5 * xsiz; xmax = xmin + nx * xsiz
ny = 100; ysiz = 10.0; ymn = 5.0; ymin = ymn - 0.5 * ysiz; ymax = ymin + ny * ysiz
ndmin = 1; ndmax = 100
radius = 2000; power = 2

invdist_map_porosity = invdist(df,'X','Y','Porosity',tmin,tmax,nx,xmn,xsiz,ny,ymn,ysiz,ndmin,ndmax,radius,power)

# plot the reuslting estimated maps
plt.subplot(131)
GSLIB.locpix_st(invdist_map_porosity,xmin,xmax,ymin,ymax,xsiz,0.0,0.25,df,'X','Y','Porosity','Inverse Distance Porosity, Power = ' + str(power),'X(m)','Y(m)','Porosity',cmap)

plt.subplots_adjust(left=0.0, bottom=0.0, right=3.5, top=1.2, wspace=0.2, hspace=0.2)
plt.show()
```

#### Comments

This was a basic demonstration of spatial estimation. Much more could be done, I have other demonstrations on the basics of working with DataFrames, ndarrays, univariate statistics, plotting data, declustering, data transformations and many other workflows available at https://github.com/GeostatsGuy/PythonNumericalDemos and https://github.com/GeostatsGuy/GeostatsPy.

I hope this was helpful,

*Michael*

#### The Author:

### Michael Pyrcz, Associate Professor, University of Texas at Austin
*Novel Data Analytics, Geostatistics and Machine Learning Subsurface Solutions*

With over 17 years of experience in subsurface consulting, research and development, Michael has returned to academia driven by his passion for teaching and enthusiasm for enhancing engineers' and geoscientists' impact in subsurface resource development.

For more about Michael check out these links:

#### [Twitter](https://twitter.com/geostatsguy) | [GitHub](https://github.com/GeostatsGuy) | [Website](http://michaelpyrcz.com) | [GoogleScholar](https://scholar.google.com/citations?user=QVZ20eQAAAAJ&hl=en&oi=ao) | [Book](https://www.amazon.com/Geostatistical-Reservoir-Modeling-Michael-Pyrcz/dp/0199731446) | [YouTube](https://www.youtube.com/channel/UCLqEr-xV-ceHdXXXrTId5ig)  | [LinkedIn](https://www.linkedin.com/in/michael-pyrcz-61a648a1)

#### Want to Work Together?

I hope this content is helpful to those that want to learn more about subsurface modeling, data analytics and machine learning. Students and working professionals are welcome to participate.

* Want to invite me to visit your company for training, mentoring, project review, workflow design and / or consulting? I'd be happy to drop by and work with you!

* Interested in partnering, supporting my graduate student research or my Subsurface Data Analytics and Machine Learning consortium (co-PIs including Profs. Foster, Torres-Verdin and van Oort)? My research combines data analytics, stochastic modeling and machine learning theory with practice to develop novel methods and workflows to add value. We are solving challenging subsurface problems!

* I can be reached at mpyrcz@austin.utexas.edu.

I'm always happy to discuss,

*Michael*

Michael Pyrcz, Ph.D., P.Eng. Associate Professor The Hildebrand Department of Petroleum and Geosystems Engineering, Bureau of Economic Geology, The Jackson School of Geosciences, The University of Texas at Austin

#### More Resources Available at: [Twitter](https://twitter.com/geostatsguy) | [GitHub](https://github.com/GeostatsGuy) | [Website](http://michaelpyrcz.com) | [GoogleScholar](https://scholar.google.com/citations?user=QVZ20eQAAAAJ&hl=en&oi=ao) | [Book](https://www.amazon.com/Geostatistical-Reservoir-Modeling-Michael-Pyrcz/dp/0199731446) | [YouTube](https://www.youtube.com/channel/UCLqEr-xV-ceHdXXXrTId5ig)  | [LinkedIn](https://www.linkedin.com/in/michael-pyrcz-61a648a1)

```python

```


## The SEAIRQ model

In the present notebook, we consider the deterministic version of the SEAIRQ model.

In the SEAIRQ model we consider, there are the following classes:

* **$S$** Susceptible
* **$E$** Exposed
* **$A$** Asymptomatic infected (this class is between $E$ and $I_a$, $I_s$, and models the time an individual is already infectious, but it is not clear yet whether the individual will remain asymptomatic)
* **$I_a$** Asymptomatic infected
* **$I_s$** Symptomatic infected
* **$R$** Recovered
* **$Q$** Quarantined

In the SEAIR model, once an individual is exposed, it undergoes the transitions

\begin{equation}
    E \rightarrow A \rightarrow I_x \rightarrow R,
\end{equation}

where $I_x$ is either $I_a$ or $I_s$.

In the SEAIRQ model, there is a finite rate from the states $S$, $E$, $A$, $I_a$, $I_s$ to a quarantined class $Q$, in which an individual does not contribute to any infection rates anymore.

# 0. Definition of the model

```python
%%capture
## compile PyRoss for this notebook
import os
owd = os.getcwd()
os.chdir('../../')
%run setup.py install
os.chdir(owd)
```

```python
%matplotlib inline
import numpy as np
import pyross
import matplotlib.pyplot as plt
#from matplotlib import rc; rc('text', usetex=True)
```

```python
M = 1                  # the SIR model has no age structure
Ni = 10000*np.ones(M)   # so there is only one age group
N = np.sum(Ni)         # and the total population is the size of this age group

alpha = 0              # fraction of asymptomatic infectives
beta  = 0.2            # infection rate

gE    = 0.04             # progression rate from E to A
gA    = 0.2             # rate to go from A to Ia
gIa   = 0.1            # recovery rate of asymptomatic infectives
gIs   = 0.1            # recovery rate of symptomatic infectives
fsa   = 1              # the self-isolation parameter

E0  = np.array([0])
A0  = np.array([1])
Ia0 = np.array([0])     # the SIR model has only one kind of infective
Is0 = np.array([20])     # we take these to be symptomatic
Q0  = np.array([0])
R0  = np.array([0])     # and assume there are no recovered individuals initially
S0  = N-(Ia0+Is0+R0+E0)    # The initial susceptibles are obtained from S + E + A + Ia + Is + R = N

# there is no contact structure
def contactMatrix(t):
    return np.identity(M)

# duration of simulation and number of datapoints
Tf = 500;  Nt=Tf+1;
```

# 1. SEAIR model

```python
# For the SEAIR model, we set all rates to the Q-class to zero
tE    = 0.00           # rate E  -> Q
tA    = 0.00           # rate A  -> Q
tIa   = 0.00           # rate Ia -> Q
tIs   = 0.00           # rate Is -> Q
#tIs   = 0.05           # rate Is -> Q

# instantiate model
parameters = {'alpha':alpha, 'beta':beta,
              'gE':gE,'gA':gA,
              'gIa':gIa, 'gIs':gIs, 'gE':gE, 'fsa':fsa}
model = pyross.deterministic.SEAIR(parameters, M, Ni)

# simulate model
data=model.simulate(S0, E0, A0, Ia0, Is0, contactMatrix, Tf, Nt)
```

```python
# plot the results of the simulation

S  = data['X'][:,0].flatten()
E  = data['X'][:,1].flatten()
A  = data['X'][:,2].flatten()
Ia = data['X'][:,3].flatten()
Is = data['X'][:,4].flatten()
t = data['t']

fig = plt.figure(num=None, figsize=(10, 8), dpi=80, facecolor='w', edgecolor='k')
plt.rcParams.update({'font.size': 22})

plt.fill_between(t, 0, S/N, color="#348ABD", alpha=0.3)
plt.plot(t, S/N, '-', color="#348ABD", label='$S$', lw=4)

plt.plot(t, E/N, '-', color="green", label='$E$', lw=4)
plt.fill_between(t, 0, E/N, color='green', alpha=0.3)

plt.fill_between(t, 0, A/N, color='#A60628', alpha=0.3)
plt.plot(t, A/N, '-', color='#A60628', label='$A$', lw=4)

plt.fill_between(t, 0, (Ia+Is)/N, color='orange', alpha=0.1)
plt.plot(t, (Ia+Is)/N, '-', color='orange', label='$I$', ls='--',lw=4)

R=N-S-E-A-Ia-Is; plt.fill_between(t, 0, R/N, color="dimgrey", alpha=0.3)
plt.plot(t, R/N, '-', color="dimgrey", label='$R$', lw=4)

plt.legend(fontsize=26); plt.grid()
plt.xlabel(r'time')
plt.ylabel(r'fraction of total population')
plt.autoscale(enable=True, axis='x', tight=True)
```

# 2. SEAIRQ model

We now set the rate at which symptomatic infectives get quarantined to $1/20$ days. All other classes stay as before, i.e. never get quarantined.

(Technically, this is similar to changing the self-isolation parameter "fsa". Although the parameter "fsa" only isolates symptomatics, the $Q$ class is more general and mimics contact tracing. This can be used to quarantine al the classes starting from $S$ to $Is$.)

```python
# We now consider a nonzero rate Is -> Q
tE    = 0.00           # rate E  -> Q
tA    = 0.00           # rate A  -> Q
tIa   = 0.00           # rate Ia -> Q
tIs   = 0.05           # rate Is -> Q

# instantiate model
parameters = {'alpha':alpha, 'beta':beta,
              'gE':gE,'gA':gA,
              'gIa':gIa, 'gIs':gIs, 'gE':gE, 'fsa':fsa,
              'tE':tE, 'tA':tA,'tIa':tIa,'tIs':tIs}
model = pyross.deterministic.SEAIRQ(parameters, M, Ni)

# simulate model
data=model.simulate(S0, E0, A0, Ia0, Is0, Q0, contactMatrix, Tf, Nt)
```

```python
# plot the results of the simulation

S  = data['X'][:,0].flatten()
E  = data['X'][:,1].flatten()
A  = data['X'][:,2].flatten()
Ia = data['X'][:,3].flatten()
Is = data['X'][:,4].flatten()
Q  = data['X'][:,5].flatten()
t = data['t']

fig = plt.figure(num=None, figsize=(10, 8), dpi=80, facecolor='w', edgecolor='k')
plt.rcParams.update({'font.size': 22})

plt.fill_between(t, 0, S/N, color="#348ABD", alpha=0.3)
plt.plot(t, S/N, '-', color="#348ABD", label='$S$', lw=4)

plt.plot(t, E/N, '-', color="green", label='$E$', lw=4)
plt.fill_between(t, 0, E/N, color='green', alpha=0.3)

plt.fill_between(t, 0, A/N, color='#A60628', alpha=0.3)
plt.plot(t, A/N, '-', color='#A60628', label='$A$', lw=4)

plt.fill_between(t, 0, (Ia+Is)/N, color='orange', alpha=0.1)
plt.plot(t, (Ia+Is)/N, '-', color='orange', label='$I$', ls='--',lw=4)

R=N-S-E-A-Ia-Is; plt.fill_between(t, 0, R/N, color="dimgrey", alpha=0.3)
plt.plot(t, R/N, '-', color="dimgrey", label='$R$', lw=4)

plt.legend(fontsize=26); plt.grid()
plt.xlabel(r'time')
plt.ylabel(r'fraction of total population')
plt.autoscale(enable=True, axis='x', tight=True)
```

# 3. SEAIRQ model with more nonzero rates to quarantine class

We now set the rate at which symptomatic infectives get quarantined to $1/20$ days, and the rates at which the $E$, $A$, $I_a$, $I_s$ get quarantined to $1/50$ days.

```python
# For the SEAIR model, we set all rates to the Q-state to zero
tE    = 0.02           # rate E  -> Q
tA    = 0.02           # rate A  -> Q
tIa   = 0.02           # rate Ia -> Q
tIs   = 0.05           # rate Is -> Q

# instantiate model
parameters = {'alpha':alpha, 'beta':beta,
              'gE':gE,'gA':gA,
              'gIa':gIa, 'gIs':gIs, 'gE':gE, 'fsa':fsa,
              'tE':tE,'tA':tA,'tIa':tIa,'tIs':tIs}
model = pyross.deterministic.SEAIRQ(parameters, M, Ni)

# simulate model
data=model.simulate(S0, E0, A0, Ia0, Is0, Q0, contactMatrix, Tf, Nt)
```

```python
# plot the results of the simulation

S  = data['X'][:,0].flatten()
E  = data['X'][:,1].flatten()
A  = data['X'][:,2].flatten()
Ia = data['X'][:,3].flatten()
Is = data['X'][:,4].flatten()
Q  = data['X'][:,5].flatten()
t = data['t']

fig = plt.figure(num=None, figsize=(10, 8), dpi=80, facecolor='w', edgecolor='k')
plt.rcParams.update({'font.size': 22})

plt.fill_between(t, 0, S/N, color="#348ABD", alpha=0.3)
plt.plot(t, S/N, '-', color="#348ABD", label='$S$', lw=4)

plt.plot(t, E/N, '-', color="green", label='$E$', lw=4)
plt.fill_between(t, 0, E/N, color='green', alpha=0.3)

plt.fill_between(t, 0, A/N, color='#A60628', alpha=0.3)
plt.plot(t, A/N, '-', color='#A60628', label='$A$', lw=4)

plt.fill_between(t, 0, (Ia+Is)/N, color='orange', alpha=0.1)
plt.plot(t, (Ia+Is)/N, '-', color='orange', label='$I$', ls='--',lw=4)

R=N-S-E-A-Ia-Is; plt.fill_between(t, 0, R/N, color="dimgrey", alpha=0.3)
plt.plot(t, R/N, '-', color="dimgrey", label='$R$', lw=4)

plt.legend(fontsize=26); plt.grid()
plt.xlabel(r'time')
plt.ylabel(r'fraction of total population')
plt.autoscale(enable=True, axis='x', tight=True)
```

We now observe qualitatively different behaviour as compared to 1 and 2. There is no clear peak in the number of exposed anymore, and the number of susceptibles decreases slowly. The processes has not reached a steady state yet, even after 500 days.

We now replot only the $E$, $A$, and $I = I_a + I_s$ classes, which are hardly visible in the above plot:

```python
# plot only the E, A, and I = Ia + Is classes

fig = plt.figure(num=None, figsize=(10, 8), dpi=80, facecolor='w', edgecolor='k')
plt.rcParams.update({'font.size': 22})

#plt.fill_between(t, 0, S/N, color="#348ABD", alpha=0.3)
#plt.plot(t, S/N, '-', color="#348ABD", label='$S$', lw=4)

plt.plot(t, E/N, '-', color="green", label='$E$', lw=4)
plt.fill_between(t, 0, E/N, color='green', alpha=0.3)

plt.fill_between(t, 0, A/N, color='#A60628', alpha=0.3)
plt.plot(t, A/N, '-', color='#A60628', label='$A$', lw=4)

plt.fill_between(t, 0, (Ia+Is)/N, color='orange', alpha=0.1)
plt.plot(t, (Ia+Is)/N, '-', color='orange', label='$I$', ls='--',lw=4)

#R=N-S-E-A-Ia-Is; plt.fill_between(t, 0, R/N, color="dimgrey", alpha=0.3)
#plt.plot(t, R/N, '-', color="dimgrey", label='$R$', lw=4)

plt.legend(fontsize=26); plt.grid()
plt.xlabel(r'time')
plt.ylabel(r'fraction of total population')
plt.autoscale(enable=True, axis='x', tight=True)
```

As can be seen, the peak now is at about 350 days, and after 500 days the average numbers in all classes are decreasing.


# Implementation of a Devito viscoacoustic equations

## This tutorial is contributed by SENAI CIMATEC (2020)

This tutorial is based on:

**Linear inversion in layered viscoacoustic media using a time‐domain method** (1994)

Joakim O. Blanch and William W. Symes

SEG Technical Program Expanded Abstracts

https://doi.org/10.1190/1.1822695

**True-amplitude prestack depth migration** (2007)

Feng Deng and George A. McMechan

GEOPHYSICS Technical Papers

https://doi.org/10.1190/1.2714334

**Attenuation compensation for least-squares reverse time migration using the viscoacoustic-wave equation** (2014)

Gaurav Dutta and Gerard T. Schuster

GEOPHYSICS Technical Papers

https://doi.org/10.1190/geo2013-0414.1

**Multiscale viscoacoustic waveform inversion with the second generation wavelet transform and adaptive time–space domain finite-difference method** (2014)

Zhiming Ren, Yang Liu,and Qunshan Zhang

Geophysical Journal International, Volume 197, Issue 2, 1 May 2014, Pages 948–974

https://doi.org/10.1093/gji/ggu024

**Viscoacoustic prestack reverse time migration based on the optimal time-space domain high-order finite-difference method** (2014)

Yan Zhao, Yang Liu, and Zhi-Ming Ren

Appl. Geophys. 11, 50–62.

https://doi.org/10.1007/s11770-014-0414-8

**A stable and efficient approach of Q reverse time migration** (2018)

Yan Zhao, Ningbo Mao, and Zhiming Ren

GEOPHYSICS Technical Papers

https://doi.org/10.1190/geo2018-0022.1

## Introduction

The conversion of mechanical energy to heat, occurs during the propagation of seismic waves on the subsurface, due to the viscosity of the rocks. The presence of oil and gas in these rocks causes seismic attenuations. Thus, associated effects, such as dispersion and dissipation, can significantly affect the amplitudes, as well as the phase of the seismic pulse.  However, in the seismic exploration, the subsurface has still been considered as an ideal elastic/acoustic medium, that is, disregarding its mitigating effect. In practice, the propagation of seismic waves on the subsurface is in many ways different from propagation in an ideal solid.

For example, some subsurface rocks have anisotropic properties, are heterogeneous, porous and so on. The acoustic/elastic wave equation is not sensitive enough to describe propagation in these more complicated mediums. Generally, the viscosity of materials in the subsurface causes energy dissipation and consequently a decrease in amplitude, in addition to modifying the frequency content of the waves. This phenomenon of energy dissipation of the wave is called seismic absorption or attenuation.

The goal of this tutorial is to perform a seismic modeling taking into account the viscosity of the medium, so that it is possible to more accurately simulate the seismic data and consequently build images with better resolution in the processing of this data, in addition to extracting more detailed information on rocky materials through seismic inversion.

This tutorial follow three main viscoacoustic approaches in time-space domain:

- Blanch and Symes (1995) / Dutta and Schuster (2014)

- Ren et al. (2014)

- Deng and McMechan (2007)

Table of symbols

| Symbol         | Description
| :---         | :---
|$f$                                  |Frequency                                            |
|$f_o$                                |Reference frequency                                  |
|$\omega$                             |Angular frenquency                                   |
|$\omega_0$                           |Angular Reference Frequency                          |
|$v$                                  |Velocity model                                       |
|$v_0$                                |Reference velocity at $\omega_0$                     |
|$\kappa$                             |Bulk modulus                                         |
|$g$                                  |Absorption coefficient                               |
|$\tau$                               |Relaxation time                                      |
|$\tau_\sigma$                        |Stress relaxation parameter                          |
|$\tau_\epsilon$                      |Strain relaxation parameter                          |
|$Q$                                  |Quality factor                                       |
|$\eta$                               |Viscosity                                            |
|$\rho$                               |Density                                              |
|$\nabla$                             |Nabla operator                                       |
|$P({\bf x},t)$                       |Pressure field                                       |
|$r({\bf x},t)$                       |Memory variable                                      |
|${\bf v}({\bf x},t)$                 |Particle velocity                                    |
|$S({\bf x}_s,t)$                     |Source                                               |

# Seismic modelling with Devito

Before start with the viscoacoustic approaches we will describe a setup of seismic modelling with Devito in a simple 2D case. We will create a physical model of our domain and define a single source and an according set of receivers to model for the forward model. But first, we initialize some basic utilities.

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

import matplotlib.pyplot as plt

from devito import *
from examples.seismic.source import RickerSource, WaveletSource, TimeAxis
from examples.seismic import ModelViscoacoustic, plot_image, setup_geometry, plot_velocity
```

```python
nx = 300
nz = 300

# Define a physical size
shape = (nx, nz)
spacing = (20., 20.)
origin = (0., 0.)
nlayers = 3
nbl = 50
space_order = 8
dtype = np.float32

# Model physical parameters:
vp = np.zeros(shape)
qp = np.zeros(shape)
rho = np.zeros(shape)

# Define a velocity profile. The velocity is in km/s
vp_top = 1.5
vp_bottom = 3.5

# Define a velocity profile in km/s
v = np.empty(shape, dtype=dtype)
v[:] = vp_top  # Top velocity (background)
vp_i = np.linspace(vp_top, vp_bottom, nlayers)
for i in range(1, nlayers):
    v[..., i*int(shape[-1] / nlayers):] = vp_i[i]  # Bottom velocity

qp[:] = 3.516*((v[:]*1000.)**2.2)*10**(-6) # Li's empirical formula

rho[:] = 0.31*(v[:]*1000.)**0.25 # Gardner's relation
```

```python
#NBVAL_IGNORE_OUTPUT
model = ModelViscoacoustic(space_order=space_order, vp=v, qp=qp, b=1/rho,
                           origin=origin, shape=shape, spacing=spacing,
                           nbl=nbl)
```

    Operator `initdamp` run in 0.01 s
    Operator `padfunc` run in 0.01 s
    Operator `padfunc` run in 0.01 s
    Operator `padfunc` run in 0.01 s

```python
#NBVAL_IGNORE_OUTPUT
aspect_ratio = model.shape[0]/model.shape[1]

plt_options_model = {'cmap': 'jet', 'extent': [model.origin[0], model.origin[0] + model.domain_size[0],
                                               model.origin[1] + model.domain_size[1], model.origin[1]]}
fig, ax = plt.subplots(nrows=1, ncols=3, figsize=(15, 5))

slices = [slice(model.nbl, -model.nbl), slice(model.nbl, -model.nbl)]

img1 = ax[0].imshow(np.transpose(model.vp.data[slices]), vmin=1.5, vmax=3.5, **plt_options_model)
fig.colorbar(img1, ax=ax[0])
ax[0].set_title(r"V (km/s)", fontsize=20)
ax[0].set_xlabel('X (m)', fontsize=20)
ax[0].set_ylabel('Depth (m)', fontsize=20)
ax[0].set_aspect('auto')

img2 = ax[1].imshow(np.transpose(qp), vmin=15, vmax=220, **plt_options_model)
fig.colorbar(img2, ax=ax[1])
ax[1].set_title("Q", fontsize=20)
ax[1].set_xlabel('X (m)', fontsize=20)
ax[1].set_ylabel('Depth (m)', fontsize=20)
ax[1].set_aspect('auto')

img3 = ax[2].imshow(np.transpose(rho), vmin=1.9, vmax=2.4, **plt_options_model)
fig.colorbar(img3, ax=ax[2])
ax[2].set_title(r"Density $\rho$ (g/cm^3)", fontsize=20)
ax[2].set_xlabel('X (m)', fontsize=20)
ax[2].set_ylabel('Depth (m)', fontsize=20)
ax[2].set_aspect('auto')

plt.tight_layout()
```

```python
f0 = 0.005 # peak/dominant frequency
b = model.b
rho = 1./b

# velocity model
vp = model.vp

t_s = (sp.sqrt(1.+1./model.qp**2)-1./model.qp)/f0
t_ep = 1./(f0**2*t_s)
tt = (t_ep/t_s) - 1.

s = model.grid.stepping_dim.spacing
damp = model.damp
```

```python
# Time step in ms and time range:
t0, tn = 0., 2000.
dt = model.critical_dt
time_range = TimeAxis(start=t0, stop=tn, step=dt)
```

```python
from examples.seismic import Receiver

def src_rec(p, model):
    src = RickerSource(name='src', grid=model.grid, f0=f0, time_range=time_range)
    src.coordinates.data[0, :] = np.array(model.domain_size) * .5
    src.coordinates.data[0, -1] = 8.

    # Create symbol for receivers
    rec = Receiver(name='rec', grid=model.grid, npoint=shape[0], time_range=time_range)

    # Prescribe even spacing for receivers along the x-axis
    rec.coordinates.data[:, 0] = np.linspace(0, model.domain_size[0], num=shape[0])
    rec.coordinates.data[:, 1] = 8.

    src_term = src.inject(field=p.forward, expr=(s*src))
    rec_term = rec.interpolate(expr=p)

    return src_term + rec_term, src, rec
```

Auxiliary functions for plotting data:

```python
def plot_receiver(rec):
    rec_plot = rec.resample(num=1001)
    scale_for_plot = np.diag(np.linspace(1.0, 2.5, 1001)**2.0)
    # Pressure (txx + tzz) data at sea surface
    extent = [rec_plot.coordinates.data[0, 0], rec_plot.coordinates.data[-1, 0], 1e-3*tn, t0]
    aspect = rec_plot.coordinates.data[-1, 0]/(1e-3*tn)/.5

    plt.figure(figsize=(10, 10))
    plt.imshow(np.dot(scale_for_plot, rec_plot.data), vmin=-.01, vmax=.01, cmap="seismic",
           interpolation='lanczos', extent=extent, aspect=aspect)
    plt.ylabel("Time (s)", fontsize=20)
    plt.xlabel("Receiver position (m)", fontsize=20)
```

```python
def plot_v_and_p(model, v, p):

    slices = [slice(model.nbl, -model.nbl), slice(model.nbl, -model.nbl)]
    scale = .5*1e-3

    plt_options_model = {'extent': [model.origin[0] , model.origin[0] + model.domain_size[0],
                                    model.origin[1] + model.domain_size[1], model.origin[1]]}

    fig, ax = plt.subplots(nrows=1, ncols=3, figsize=(15, 7))

    ax[0].imshow(np.transpose(v[0].data[0][slices]), vmin=-scale, vmax=scale, cmap="RdGy", **plt_options_model)
    ax[0].imshow(np.transpose(model.vp.data[slices]), vmin=1.5, vmax=3.5, cmap="jet", alpha=.5, **plt_options_model)
    ax[0].set_aspect('auto')
    ax[0].set_xlabel('X (m)', fontsize=20)
    ax[0].set_ylabel('Depth (m)', fontsize=20)
    ax[0].set_title(r"$v_{x}$", fontsize=20)

    ax[1].imshow(np.transpose(v[1].data[0][slices]), vmin=-scale, vmax=scale, cmap="RdGy", **plt_options_model)
    ax[1].imshow(np.transpose(model.vp.data[slices]), vmin=1.5, vmax=3.5, cmap="jet", alpha=.5, **plt_options_model)
    ax[1].set_aspect('auto')
    ax[1].set_xlabel('X (m)', fontsize=20)
    ax[1].set_title(r"$v_{z}$", fontsize=20)

    ax[2].imshow(np.transpose(p.data[0][slices]), vmin=-scale, vmax=scale, cmap="RdGy", **plt_options_model)
    ax[2].imshow(np.transpose(model.vp.data[slices]), vmin=1.5, vmax=3.5, cmap="jet", alpha=.5, **plt_options_model)
    ax[2].set_aspect('auto')
    ax[2].set_xlabel('X (m)', fontsize=20)
    ax[2].set_title(r"$P$", fontsize=20)
```

## Blanch and Symes (1995) / Dutta and Schuster (2014)

The equations of motion for a viscoacoustic medium can be written as:

\begin{equation}
\left\{
\begin{array}{lcl}
\frac{\partial P}{\partial t} + \kappa (\tau + 1)(\nabla \cdot {\bf v}) + r = S({\bf x}_{s}, t) \\
\frac{\partial {\bf v}}{\partial t} + \frac{1}{\rho}\nabla{P} = 0 \\
\frac{\partial r}{\partial t} + \frac{1}{\tau_{\sigma}} [r + \tau \kappa (\nabla \cdot {\bf v})] = 0.
\end{array}
\right.
\label{first-order-dutta}
\end{equation}

Where $\tau = \tau_{\epsilon}/\tau_{\sigma} -1$ represents the magnitude of  $Q$. $\tau_{\epsilon}$ and $\tau_{\sigma}$ are, respectively, the stress and strain relaxation parameters, given by:

\begin{equation}
\tau_\sigma = \frac{\sqrt{Q^2+1}-1}{2 \pi f_0 Q}
\end{equation}
and
\begin{equation}
\tau_\epsilon= \frac{\sqrt{Q^2+1}+1}{2\pi f_0 Q}
\end{equation}

```python
# Stencil created from Blanch and Symes (1995) / Dutta and Schuster (2014)
def blanch_symes(model, p, r, v):

    # Bulk modulus
    bm = rho * (vp * vp)

    # Define PDE to v
    pde_v = v - s * b * grad(p)
    u_v = Eq(v.forward, damp * pde_v)

    # Define PDE to r
    pde_r = r - s * (1. / t_s) * r - s * (1. / t_s) * tt * bm * div(v.forward)
    u_r = Eq(r.forward, damp * pde_r)

    # Define PDE to p
    pde_p = p - s * bm * (tt + 1.) * div(v.forward) - s * r.forward
    u_p = Eq(p.forward, damp * pde_p)

    return [u_v, u_r, u_p]
```

```python
# Seismic Modelling from Blanch and Symes (1995) / Dutta and Schuster (2014) viscoacoustic wave equation.
def modelling_blanch_symes(model):

    # Create symbols for particle velocity, pressure field, memory variable, source and receivers

    v = VectorTimeFunction(name="v", grid=model.grid, time_order=1, space_order=space_order)

    p = TimeFunction(name="p", grid=model.grid, time_order=1, space_order=space_order,
                     staggered=NODE)

    r = TimeFunction(name="r", grid=model.grid, time_order=1, space_order=space_order,
                     staggered=NODE)

    # define the source injection and create interpolation expression for receivers

    src_rec_expr, src, rec = src_rec(p, model)

    eqn = blanch_symes(model, p, r, v)

    op = Operator(eqn + src_rec_expr, subs=model.spacing_map)

    op(time=time_range.num-1, dt=dt, src=src, rec=rec)

    return rec, v, p
```

```python
#NBVAL_IGNORE_OUTPUT
rec, v, p = modelling_blanch_symes(model)
```

    Operator `Kernel` run in 2.35 s

```python
#NBVAL_IGNORE_OUTPUT
plot_receiver(rec)
```

```python
assert np.isclose(np.linalg.norm(rec.data), 16, rtol=10)
```

```python
#NBVAL_IGNORE_OUTPUT
plot_v_and_p(model, v, p)
```

```python
assert np.isclose(norm(v[0]), 1.87797, atol=1e-3, rtol=0)
```

## Ren et al. (2014)

The viscoacoustic wave equation in time domain is written as:

\begin{equation}
\frac{\partial^{2}P}{\partial{t^2}} - v^{2}\nabla^{2}{P} - \eta\nabla^{2}\left(\frac{\partial P}{\partial t}\right) = S({\bf x}_{s}, t),
\end{equation}

where $\eta = \frac{v^2}{\omega_{0}Q}$ represents the viscosity of medium.

Considering the variable density $\rho$, the equation can be rewritten as:

\begin{equation}
\frac{\partial^{2}P}{\partial{t^2}} - \kappa \nabla \cdot \frac{1}{\rho} \nabla{P} - \eta \rho \nabla \cdot \frac{1}{\rho} \nabla \left(\frac{\partial{P}}{\partial{t}}\right) = S({\bf x}_{s}, t).
\end{equation}

The equation can be written using a first order formulation, given by:

\begin{equation}
\left\{
\begin{array}{ll}
\frac{\partial P}{\partial t} + \kappa \nabla \cdot {\bf v} - \eta \rho \nabla \cdot \frac{1}{\rho} \nabla{P} = S({\bf x}_{s}, t) \\
\frac{\partial {\bf v}}{\partial t} + \frac{1}{\rho} \nabla{P} = 0
\end{array}
\right.
\end{equation}

```python
# Stencil created from Ren et al. (2014) viscoacoustic wave equation.
def ren(model, p, v):

    # Angular frequency
    w = 2. * np.pi * f0

    # Define PDE to v
    pde_v = v - s * b * grad(p)
    u_v = Eq(v.forward, damp * pde_v)

    # Define PDE to p
    pde_p = p - s * vp * vp * rho * div(v.forward) + s * \
            ((vp * vp * rho) / (w * model.qp)) * div(b * grad(p))

    u_p = Eq(p.forward, damp * pde_p)

    return [u_v, u_p]
```

```python
# Seismic Modelling from Ren et al. (2014) viscoacoustic wave equation.
def modelling_ren(model):

    # Create symbols for particle velocity, pressure field, source and receivers

    v = VectorTimeFunction(name="v", grid=model.grid, time_order=1, space_order=space_order)

    p = TimeFunction(name="p", grid=model.grid, time_order=1, space_order=space_order,
                     staggered=NODE)

    # define the source injection and create interpolation expression for receivers

    src_rec_expr, src, rec = src_rec(p, model)

    eqn = ren(model, p, v)

    op = Operator(eqn + src_rec_expr, subs=model.spacing_map)

    op(time=time_range.num-1, dt=dt, src=src, rec=rec)

    return rec, v, p
```

```python
#NBVAL_IGNORE_OUTPUT
rec, v, p = modelling_ren(model)
```

    Operator `Kernel` run in 1.83 s

```python
#NBVAL_IGNORE_OUTPUT
plot_receiver(rec)
```

```python
assert np.isclose(np.linalg.norm(rec.data), 15, rtol=10)
```

```python
#NBVAL_IGNORE_OUTPUT
plot_v_and_p(model, v, p)
```

```python
assert np.isclose(norm(v[0]), 1.0639238, atol=1e-3, rtol=0)
```

## Deng and McMechan (2007)

The viscoacoustic wave equation for the propagating pressure $P$ in the time-space domain:

\begin{equation}
\frac{1}{v^2}\frac{\partial^{2}P}{\partial{t^2}} - \nabla^{2}P + \frac{g}{v}\frac{\partial P}{\partial{t}}  = S({\bf x}_{s}, t),
\label{eq-deng}
\end{equation}

where $g$ is the absorption coefficient, given by:

\begin{equation}
g = \frac{2\pi f_{0}}{vQ},
\end{equation}

The equation can be written using a first order formulation, given by:

\begin{equation}
\left\{
\begin{array}{lcl}
\frac{\partial P}{\partial t} + \kappa (\nabla \cdot {\bf v}) + \frac{2\pi f_{0}}{Q}P= S({\bf x}_{s}, t) \\
\frac{\partial {\bf v}}{\partial t} + \frac{1}{\rho}\nabla{P} = 0 \\
\end{array}
\right.
\end{equation}

```python
# Stencil created from Deng and McMechan (2007) viscoacoustic wave equation.
def deng_mcmechan(model, p, v):

    # Angular frequency
    w = 2. * np.pi * f0

    # Define PDE to v
    pde_v = v - s * b * grad(p)
    u_v = Eq(v.forward, damp * pde_v)

    # Define PDE to p
    pde_p = p - s * vp * vp * rho * div(v.forward) - s * (w / model.qp) * p
    u_p = Eq(p.forward, damp * pde_p)

    return [u_v, u_p]
```

```python
# Seismic Modelling from Deng and McMechan (2007) viscoacoustic wave equation.
def modelling_deng_mcmechan(model):

    # Create symbols for particle velocity, pressure field, source and receivers

    v = VectorTimeFunction(name="v", grid=model.grid, time_order=1, space_order=space_order)

    p = TimeFunction(name="p", grid=model.grid, time_order=1, space_order=space_order,
                     staggered=NODE)

    # define the source injection and create interpolation expression for receivers

    src_rec_expr, src, rec = src_rec(p, model)

    eqn = deng_mcmechan(model, p, v)

    op = Operator(eqn + src_rec_expr, subs=model.spacing_map)

    op(time=time_range.num-1, dt=dt, src=src, rec=rec)

    return rec, v, p
```

```python
#NBVAL_IGNORE_OUTPUT
rec, v, p = modelling_deng_mcmechan(model)
```

    Operator `Kernel` run in 2.01 s

```python
#NBVAL_IGNORE_OUTPUT
plot_receiver(rec)
```

```python
assert np.isclose(np.linalg.norm(rec.data), 16, rtol=10)
```

```python
#NBVAL_IGNORE_OUTPUT
plot_v_and_p(model, v, p)
```

```python
assert np.isclose(norm(v[0]), 1.1323929, atol=1e-3, rtol=0)
```

# More references

[1] https://academic.oup.com/gji/article/197/2/948/616510

[2] https://link.springer.com/article/10.1007/s11770-014-0414-8

[3] https://janth.home.xs4all.nl/Software/fdelmodcManual.pdf