# Insurance and Incentives

*By Sebastian Graves and Thomas Sargent*

This notebook computes optimal contracts for the three examples that lead off chapter 21 of
**Recursive Macroeconomic Theory, Fourth edition** by Lars Ljungqvist and Thomas Sargent.

The examples illustrate different sorts of tradeoffs between insurance and incentives that emerge under different
limits on enforcement and information.

In each of the three economies, a planner or money-lender designs an efficient contract to supply insurance to a risk-averse consumer who receives an exogenous  random stream of a non-storable endowment.

The only way that the consumer to smooth consumption across states and time is to interact with the planner.

The three models differ in the constraints that they impose  on the planner.

These constraints express the planner's  limited ability either to enforce a contract or to observe the consumer's endowment

Each of the examples uses a version of what we have nicknamed **dynamic programming squared**

In a dynamic programming squared problem, a value function from one Bellman equation is an argument of another Bellman equation.

In the examples below, a planner or money lender's value function will have as an argument the value of a villager
that satisfies a Bellman equation

### Three models of a villager and a money lender

Imagine a  village with a large number of ex ante
identical households. Each household has preferences over
consumption streams that are ordered by
$$ E_{-1}\sum_{t=0}^\infty \beta^t u(c_t),  $$
where $u(c)$ is an increasing, strictly concave, and twice
continuously differentiable  function,
 $\beta \in (0,1)$ is a discount factor, and $E_{-1}$ is the mathematical expectation
 not conditioning on any information available at time $0$ or later.

Each household
receives a stochastic endowment stream $\{y_t\}_{t=0}^\infty$,
where for each $t \geq 0$, $y_t$ is independently and
identically distributed according to the discrete
probability distribution ${\rm Prob} (y_t = \overline y_s) = \Pi_s,$
where $s \in \{1, 2, \ldots ,S\}\equiv {\bf S}$ and
$\overline y_{s+1}>\overline y_s$.

The consumption
good is not storable.

At time $t \geq 1$, the
household has received a history of endowments
$h_t = (y_t, y_{t-1}, \ldots, y_0).$

Endowment processes are distributed independently and identically
 both across time and
across households.

##### Competitive equilibrium

In  this setting, if there were a competitive equilibrium with
complete markets, at date
$0$ households would trade history- and date-contingent claims.

Since  households are  ex ante
identical, each household would consume the per capita
endowment in every period, and its lifetime utility would be

$$ v_{\rm pool} = \sum_{t=0}^\infty
\beta^t \, u\!\left(\sum_{s=1}^S \Pi_s \overline y_s\right) =
 {1 \over 1-\beta}\, u\!\left(\sum_{s=1}^S \Pi_s \overline y_s\right) .
                                                         $$

 Households would thus insure away all
risks from their individual endowment processes.

But the
 incentive constraints that we are about to specify make
this allocation unattainable.

For each specification of incentive
constraints, we shall solve a planning problem for an efficient
allocation that respects those constraints.

   Following a tradition started by
Edward Green (1987) [*Lending and the Smoothing of Uninsurable
 Income*, in Edward C. Prescott and Neil Wallace, editors, **Contractual Arrangements for
 Intertemporal Trade**, Minnesota Studies in Macroeconomics series, Vol.
 1, Minneapolis: University of Minnesota Press, pp. 3--25], we assume that a *moneylender* or *planner* is
the only person in the  village who has access to
a risk-free loan market outside the village.

The moneylender can borrow or lend at a constant one-period
risk-free gross interest rate  $R=\beta^{-1}$.

Households cannot borrow or lend with each other,
and can  trade only with the moneylender.

Furthermore,
we assume that the moneylender is committed   to honor  his
promises.

We will study three distinct environments in which there are three alternative types  of incentive constraints.

**Enviroment a.** Both the money lender and the household observe   the household's  history of endowments at each  time $t$.
Although the moneylender can commit to honor a
contract, households  cannot commit and at any time are
  free to walk away from an arrangement
with the moneylender
and live in perpetual autarky thereafter.  They must be induced not to do so
by the structure of
the contract.
This is a model of *one-sided commitment* in which the
contract must be *self-enforcing*.  That is, it must be structured to induce   the household to  prefer to
conform to it.

**Environment b.** Households *can* make commitments  and enter
into enduring and   binding contracts with the moneylender,
but they have private
information about their own incomes. The moneylender
can see neither their income nor their consumption. Instead,
 exchanges between the moneylender and a household must
be based on the household's own reports about income
realizations. An incentive-compatible contract  induces
a household to report its income truthfully.

**Environment c.** The environment is the same as  b except  that now households have access to a storage technology that
cannot be observed by the moneylender.
Households can store nonnegative amounts of goods at a risk-free
gross return of $R$ equal to the interest rate that
the moneylender faces in the outside credit market.
Since the moneylender can both borrow and lend at the interest
rate $R$ outside of the village,
the private storage technology does not change the economy's
aggregate resource constraint, but it does  affect the set of
incentive-compatible contracts between the moneylender and the
households.

#### Preview

When we compute efficient allocations for each of these three
environments, we  find that the dynamics of the implied
consumption allocations differ dramatically.

 We shall see that the dynamics
of consumption outcomes evidently differ substantially across the
three environments, increasing monotonically and then flattening out in environment a,
stochastically heading south in environment b, and stochastically heading north in
environment c.
These sample path properties will reflect how the optimal  contracts cope with the three different frictions that we have put into the environment.

Chapter 21 of RMT4  explains why  sample paths of consumption differ
so much across these three settings.

### Three computed contracts

For all three environments discussed, consumers have a utility function:

$$u(c) = - \gamma^{-1} \exp(-\gamma c)$$

We set $\gamma = 0.7$, and the discount factor, $\beta$ to 0.8.

The consumers receive an iid endowment  that  can take any integer in the range $[\bar y_1,...,\bar y_{5}] = [6,...,10]$.

The probability of each realisation is $\Pi_s = \frac{1-\lambda}{1-\lambda^{5}}\lambda^{s-1}$ with $\lambda = 0.4$.

As mentioned above, an interesting  benchmark case is a complete markets environment.

Because  all households are *ex ante* identical,  in a complete markets economy each household would consume the per capita endowment in every period, and its lifetime utility would be:

$$ v_{pool} = \frac{1}{1-\beta} u \left( \sum_{s=1}^S \Pi_s \bar y_s \right) =  \frac{u(c_{pool})}{1-\beta} $$

Later we will compare the consumption paths for each environment to that which would occur in the complete markets environment.

In each environment, we compute allocations  for the situation in which  the planner or money lender just breaks even.

## Environment a

The first environment is one in which the planner is able to commit, but households are not.

At any time households are free to walk away from an arrangement with the planner, and live in perpetual autarky thereafter.

RMT4 shows how this problem can be written in a recursive form.

Equations 21.3.4 to 21.3.8 in RMT4 express  the planners's problem as:

\begin{align}
	&P(v) = \max_{c_s,w_s} \sum_{s=1}^S \Pi_s \left[ (\bar y_s - c_s) + \beta P(w_s) \right] \\
	&\text{subject to} \\
	&\sum_{s=1}^S \Pi_s \left[ u(c_s) + \beta w_s \right] \geq v \\
	&u(c_s) + \beta w_s \geq u(\bar y_s) + \beta v_{aut} \text{ , s = 1,...,S} \\
	&c_s \in [c_{min},c_{max}] \\
	&w_s \in [v_{aut},\bar v]
\end{align}

where $w_s$ is the promised value with which the consumer will enter the next period, given that $y = \bar y_s$ this period.

The first constraint is a promise keeping constraint, while the second set of constraints are participation constraints. $[c_{min},c_{max}]$ is a bounded set, while $\bar v$ just needs to be a very large number.

The value of autarky to the households is:

$$ v_{aut} = \frac{1}{1-\beta} \sum_{s=1}^S \Pi_s u(\bar y_s) $$

Below we solve the moneylender's problem in this environment by approximating $P(v)$ using Chebyshev polynomials.

```python
import numpy as np
from scipy.optimize import minimize, fsolve
from scipy.interpolate import UnivariateSpline
import matplotlib.pyplot as plt
import numpy.polynomial.chebyshev as cheb
%matplotlib inline
```

```python
# Parameter values
gamma = 0.7
beta = 0.8
lamb = 0.4
S = 5
y_grid = np.linspace(6,5+S,S)
prob_grid = np.zeros(S)
for i in range(S):
    prob_grid[i] = (1 - lamb)/(1-lamb**S)*lamb**(i)

# Utility function
u = lambda c: -gamma**(-1)*np.exp(-gamma*c)
u_inv = lambda u: np.log(-gamma*u)/(-gamma)

# Calculate complete markets consumption
c_pool = np.dot(prob_grid,y_grid)

# Calculate value of autarky
v_aut = 1/(1-beta)*np.dot(prob_grid, u(y_grid))
```

```python
# Functions used in each environment

# Nodes and basis matrix for Chebyshev approximation

def Cheb_basis(order,lb,ub):
    # Calculate roots of Chebyshev polynomial
    k = np.linspace(order, 1, order)
    roots = np.cos((2*k - 1)*np.pi/(2*order))
    # Scale to approximation space
    s = lb + (roots - -1)/2*(ub-lb)
    # Create basis matrix
    Phi = cheb.chebvander(roots, order-1)
    return s, Phi

# Value Function Iteration

def Bellman_Iterations(s, Phi, P_fun, x_store, coeff, tolc=1e-6, bnds=None, cons=(), max_iters=100):
    global x, c
    c = coeff
    order = Phi.shape[1]
    iters = 0
    diff = 1

    while diff > tolc:
        # 1. Maximization, given value function guess
        P_iter = np.zeros(order)
        for i in range(order):
            x = s[i]
            res = minimize(P_fun, x_store[i], method = 'SLSQP', bounds = bnds, constraints=cons, tol=1e-15)
            x_store[i] = res.x
            P_iter[i] = -P_fun(res.x)
        # 2. Bellman updating of Value Function coefficients
        c1 = np.linalg.solve(Phi, P_iter)
        # 3. Compute distance and update
        diff = max(abs(c1 - c))
        print(diff)
        c = np.copy(c1)
        iters = iters + 1

        if iters >= max_iters:
            print('Convergence failed after {} iterations'.format(iters))
            break

    if diff < tolc:
        print('Convergence achieved after {} iterations'.format(iters))

    return c
```

```python
# Value Function Approximation
# Set bounds and approximation order
v_min = v_aut
v_max = -0.065
c_min = 0
c_max = 50
order = 70

# Calculate nodes and basis matrix
s, Phi = Cheb_basis(order, v_min, v_max)

# Bounds for Maximisation
lb = np.concatenate([np.ones(S)*c_min, np.ones(S)*v_min], axis=0)
ub = np.concatenate([np.ones(S)*c_max, np.ones(S)*v_max], axis=0)

# Initialize Value Function coefficients and goess for c,w
y = (c_pool - u_inv(s*(1-beta)))/(1-beta)
c = np.linalg.solve(Phi, y)
x_init = np.concatenate([np.ones(S)*c_min, np.ones(S)*v_min], axis=0)

# Function to minimize and constraints
def P_fun(x):
    scale = -1 + 2*(x[S:2*S] - v_min)/(v_max - v_min)
    P = np.dot(cheb.chebvander(scale,order-1),c)
    P_fun = - prob_grid.dot((y_grid - x[0:S]) + beta*P)
    return P_fun

def cons12(y):
    global x
    return prob_grid.dot(u(y[0:S]) + beta*y[S:2*S]) - x

cons1 = ({'type': 'ineq', 'fun': lambda y: u(y[0:S]) + beta*y[S:2*S] - u(y_grid) - beta*v_aut},
        {'type': 'ineq', 'fun': cons12})

bnds1 = np.concatenate([lb.reshape(2*S, 1), ub.reshape(2*S, 1)], axis = 1)

# Bellman Iterations
NBell = 5
tolc = 1e-6
diff = 1
iters = 1
x_store = {}
for i in range(order):
    x_store[i] = x_init

c = Bellman_Iterations(s, Phi, P_fun, x_store, c, bnds=bnds1, cons=cons1)
```

    0.02046457448057115
    0.01420417271952934
    0.007919316517300268
    0.004362488108637486
    0.0024521591210679983
    0.0014181717553074513
    0.0008450151371991454
    0.0005180673399127755
    0.0003258997519962614
    0.0002096164890845742
    0.00013734153609978872
    9.134953200051754e-05
    6.149551030254496e-05
    4.179812741511579e-05
    2.8630491596959295e-05
    1.973504389185532e-05
    1.367544312058655e-05
    9.5197594243146e-06
    6.6539162798529006e-06
    4.66802753196216e-06
    3.286358093546049e-06
    2.323138710569328e-06
    1.666813717604576e-06
    1.1999303339838008e-06
    8.667152331387484e-07
    Convergence achieved after 25 iterations

```python
# Time Series Simulation
T = 100
np.random.seed(2)
y_series = np.random.choice(y_grid,size = T,p = prob_grid)
c_series = np.zeros(T)
w_series = np.zeros(T)
resid_series = np.zeros(T)
pval_series = np.zeros(T)

# Initialize v such that P(v) = 0
v_find = lambda v: cheb.chebvander(-1 + 2*(v - v_min)/(v_max - v_min),order-1).dot(c)
x = fsolve(v_find,v_max)

res = minimize(P_fun,x_init,method = 'SLSQP',bounds = bnds1,constraints = cons1,tol=1e-15)
c_series[0] = res.x[np.where(y_grid == y_series[0])[0][0]]
w_series[0] = res.x[S + np.where(y_grid == y_series[0])[0][0]]

# Simulate
for t in range(1,T):
    x = w_series[t-1]
    res = minimize(P_fun, x_init,method = 'SLSQP',bounds = bnds1, constraints = cons1, tol=1e-15)
    c_series[t] = res.x[np.where(y_grid == y_series[t])[0][0]]
    w_series[t] = res.x[S + np.where(y_grid == y_series[t])[0][0]]

plt.plot(c_series, label = 'Environment (a)')
plt.plot(np.ones(T)*c_pool, label = 'Complete Markets')
plt.ylabel('Consumption')
plt.xlabel('Time')
plt.legend(loc = 'best');
plt.title('Environment (a)');
```

The above simulation is equivalent to Figure 21.2.1.a in RMT.

The discussion in RMT4 confirms that the household's consumption ratchets upwards over time.

The consumption level is constant after the first time that the household receives the highest possible endowment.

## Environment b

The second environment is one in which households *can* make commitments to enter into binding contracts with the planner, but they have private information about their incomes.

Consequently, incentive compatability constraints are required to ensure that households truthfully report their incomes.

Equations 21.5.1 to 21.5.5 in RMT4 express the planners's problem.

\begin{align}
	&P(v) = \max_{b_s,w_s} \sum_{s=1}^S \Pi_s \left[ -b_s + \beta P(w_s) \right] \\
	&\text{s.t.} \\
	&\sum_{s=1}^S \Pi_s \left[ u(\bar y_s + b_s) + \beta w_s \right] = v \\
	& C_{s,k} \equiv u(\bar y_s + b_s) + \beta w_s - [ u(\bar y_s + b_k) + \beta w_k ] \geq 0 \hspace{2mm} \forall \hspace{2mm} s,k \in S \times S\\
	&b_s \in [a - \bar y_s,\infty ] \\
	&w_s \in [- \infty, v_{max}]
\end{align}

Here $b_s$ is the transfer that the moneylender gives to a household who reports income $y_s$ if their promised value was $v$.

The promise keeping constraint remains, while the participation constraint has been replaced by a large set of incentive compatibility constraints.

RMT4 shows that we can discard many of the incentive compatibility constraints.

In solving the model below, we keep only the local upward and downward incentive compatibility constraints.

```python
# Set bounds and approximation order
b_min = -20
b_max = 20
w_min = -150;
w_max = -0.04;
v_min = -150;
v_max = -0.04;
v_pool = u(c_pool)/(1-beta)
order = 70

# Calculate nodes and basis matrix
s, Phi = Cheb_basis(order,v_min,v_max)

# Bounds for Maximisation
lb = np.concatenate([np.ones(S)*b_min,np.ones(S)*w_min], axis=0)
ub = np.concatenate([np.ones(S)*b_max,np.ones(S)*w_max], axis=0)

# For initial guess, use upper bound given in RMT:
cbar = np.zeros(order)
upper = np.zeros(order)
for i in range(order):
    cbar[i] = u_inv((1-beta)*s[i])
    upper[i] = np.dot(prob_grid,(y_grid - cbar[i])/(1-beta))
c = np.linalg.solve(Phi,upper)

# Function to minimize and constraints
def P_fun2(x):
    scale = -1 + 2*(x[S:2*S] - v_min)/(v_max - v_min)
    P = np.dot(cheb.chebvander(scale,order-1),c)
    P_fun = - prob_grid.dot(-x[0:S] + beta*P)
    return P_fun

def cons23(y):
    global x
    return prob_grid.dot(u(y_grid + y[0:S]) + beta*y[S:2*S]) - x

cons2 = ({'type': 'ineq', 'fun': lambda x:  u(y_grid[1:S] + x[1:S]) + beta*x[S+1:2*S] - u(y_grid[1:S] + x[0:S-1]) - beta*x[S:2*S-1]},
         {'type': 'ineq', 'fun': lambda x:  u(y_grid[0:S-1] + x[0:S-1]) + beta*x[S:2*S-1] - u(y_grid[0:S-1] + x[1:S]) - beta*x[S+1:2*S]},
         {'type': 'eq',  'fun': cons23})

bnds2 = np.concatenate([lb.reshape(2*S,1),ub.reshape(2*S,1)], axis = 1)

x_store = {}
for i in range(order):
    x_store[i] = np.concatenate([np.zeros(S),np.ones(S)*s[i]], axis=0)

c = Bellman_Iterations(s, Phi, P_fun2, x_store, c, tolc, bnds=bnds2, cons=cons2)
```

    0.0356136025755589
    0.030371315540371313
    0.02407399182513359
    0.01913266149864512
    0.015222370598273471
    0.012114987856719495
    0.009631147461220735
    0.007672223008313495
    0.006105497493145151
    0.00486238930683669
    0.003874043097987112
    0.0030852738234585786
    0.002458861972733928
    0.00195938541418883
    0.0015613577112887356
    0.0012446135038572947
    0.0009919573701395734
    0.0007906335097516148
    0.0006302442740278025
    0.0005023133549428849
    0.0004003561312018178
    0.0003190925702796221
    0.0002542816361241762
    0.0002026263867875855
    0.0001614498820146082
    0.00012861680815490217
    0.0001024510164100434
    8.159488184134034e-05
    6.497036918773347e-05
    5.17247087685746e-05
    4.116982454860363e-05
    3.2760247570706724e-05
    2.6062380570124333e-05
    2.072756330306902e-05
    1.6479524738599594e-05
    1.3097959921992697e-05
    1.0406276047092433e-05
    8.26447011803566e-06
    6.560744409966901e-06
    5.205745019054575e-06
    4.1285380945055294e-06
    3.27246526410363e-06
    2.5923429660679176e-06
    2.052268719410222e-06
    1.6235840192280193e-06
    1.283467490509338e-06
    1.0137738115645334e-06
    8.000368652005818e-07
    Convergence achieved after 48 iterations

```python
# Time Series Simulation
T = 800
np.random.seed(2)
y_series = np.random.choice(y_grid,size = T+1, p = prob_grid)
c_series = np.zeros(T)
w_series = np.zeros(T)

# Initialize v such that P(v) = 0
v_find = lambda v: cheb.chebvander(-1 + 2*(v - v_min)/(v_max - v_min),order-1).dot(c)
x = fsolve(v_find,v_aut)

x_init = np.concatenate([np.zeros(S),np.ones(S)*x],axis=0)
res = minimize(P_fun2,x_init,method = 'SLSQP',bounds = bnds2, constraints = cons2,tol=1e-10)
c_series[0] = y_series[0] + res.x[np.where(y_grid == y_series[0])[0][0]]
w_series[0] = res.x[S  + np.where(y_grid == y_series[0])[0][0]]
x_init = res.x

# Simulate
for t in range(1,T):
    x = w_series[t-1]
    res = minimize(P_fun2,x_init,method = 'SLSQP',bounds = bnds2,constraints = cons2,tol=1e-10)
    c_series[t] = y_series[t] + res.x[np.where(y_grid == y_series[t])[0][0]]
    w_series[t] = res.x[S + np.where(y_grid == y_series[t])[0][0]]
    x_init = res.x

# Plot
plt.plot(c_series, label = 'Environment (b)')
plt.plot(np.ones(T)*c_pool, label = 'Complete Markets')
plt.ylabel('Consumption')
plt.xlabel('Time')
plt.title('Environment (b)')
plt.legend(loc = 'best');
```

This simulation reported in the graph above confirms that in environment **b** the incentive compatibility constraints induce  the planner to introduce a downward tilt into consumption paths.

## Environment c

The third environment is the same as in (b), except for the additional assumption that households have access to a storage technology.

A household  can store nonnegative amounts that cannot be observed by the planner.

The text of RMT4 chaper 21 shows that the solution to this problem is the same as in an economy in which  each household can lend *or borrow* at the risk-free gross interest rate R, subject to the natural debt limit.

Thus, the planner enables the household to relax the no-borrowing constraint implied by  the restriction that it can store only nonnegative amounts

We can find the natural debt limit by iterating forward on the households budget constraint:

\begin{equation}
	c + k' = y + Rk
\end{equation}
This iteration gives:
\begin{equation}
	k = \frac{1}{R} \sum_{j=0}^\infty \frac{c - y}{R^j}
\end{equation}

Imposing non-negativity on consumption:

\begin{equation}
	k \geq  - \frac{1}{R} \sum_{j=0}^\infty \frac{y}{R^j}
\end{equation}

Finally, the natural debt limit is found by choosing the lowest possible value of the endowment, so that for any possible endowment stream the household can always pay back its debts:

\begin{equation}
	k \geq  - \frac{1}{R} \sum_{j=0}^\infty \frac{\bar y_{min}}{R^j} = - \frac{\bar y_{min}}{R-1} \equiv \phi
\end{equation}

A recursive presentation of the household's problem is then:
\begin{align}
	&V(k,y) = \max_{c,k'} u(c) + \beta E [V(k',y')] \\
	&\text{s.t.} \\
	&c + k' = y + Rk \\
	& k' \geq \phi
\end{align}

As income is iid, we can re-write the household's problem with only one state.

Define a = k + y.

Then
\begin{align}
	&V(a) = \max_{c,k'} u(c) + \beta E [V(Rk' + y')] \\
	&\text{subject to} \\
	&c + k' = a \\
	& k' \geq \phi
\end{align}

Below we solve this latter problem using Value Function Iteration, again with Chebyshev polynomials.

```python
# Update parameter values
# Set bounds and approximation order
R = 1/beta
k_min = - y_grid[0]/(R - 1)
k_max = 100
a_min = R*k_min + min(y_grid)
a_max = R*k_max + max(y_grid)
order = 150

# Calculate nodes and basis matrix
s, Phi = Cheb_basis(order,a_min,a_max)

# Create bounds
bnds3 = np.array([[k_min,k_max]])

# Value function
def P_fun3(kprime):
    global x,c
    # Function to minimize
    scale = -1 + 2*(R*kprime + y_grid - a_min)/(a_max - a_min)
    P_fun = -(u(x - kprime) + beta * prob_grid.dot(cheb.chebval(scale, c)))
    return P_fun

# Initialize guess and VF coefficients
c = np.zeros(order)

x_store = {}
for i in range(order):
    x_store[i] = k_min

c = Bellman_Iterations(s, Phi, P_fun3, x_store, c, bnds=bnds3)
```

```python
# Time Series Simulation
T = 800
np.random.seed(2)
y_series = np.random.choice(y_grid, size = T+1, p = prob_grid)

a_series = np.zeros(T+1)
c_series = np.zeros(T)

# Initialise at v_aut
def k_find(k):
    scale = -1 + 2 * (R * k + y_grid - a_min)/(a_max - a_min)
    return prob_grid.dot(cheb.chebval(scale,c)) - v_aut
k0 = fsolve(k_find,0)
a_series[0] = k0 + y_series[0]

# Simulate
for t in range(T):
    x = a_series[t]
    res = minimize(P_fun3, k_min,method='SLSQP',  bounds=bnds3,tol=1e-15)
    c_series[t] = a_series[t] - res.x
    a_series[t+1] = R * res.x + y_series[t+1]

# Plot
plt.plot(c_series, label = 'Environment (c)')
plt.plot(np.ones(T)*c_pool, label = 'Complete Markets')
plt.ylabel('Consumption')
plt.xlabel('Time')
plt.title('Environment (c)')
plt.legend(loc = 'best')
```

Notice that the introduction of a storage technology for the household means that the consumption path now has an upward trend.

This occurs because our parameter values satisfy  $\beta R = 1$.


# Homogeneous System Pivot Variable Special Solutions

+ This notebook is part of lecture 7 *Solving Ax=0, pivot variables, and special solutions* in the OCW MIT course 18.06 by Prof Gilbert Strang [1]
+ Created by me, Dr Juan H Klopper
    + Head of Acute Care Surgery
    + Groote Schuur Hospital
    + University Cape Town
    + Email me with your thoughts, comments, suggestions and corrections

Linear Algebra OCW MIT18.06 IPython notebook [2] study notes by Dr Juan H Klopper is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License.

+ [1] OCW MIT 18.06
+ [2] Fernando Pérez, Brian E. Granger, IPython: A System for Interactive Scientific Computing, Computing in Science and Engineering, vol. 9, no. 3, pp. 21-29, May/June 2007, doi:10.1109/MCSE.2007.53. URL: http://ipython.org

```python
from IPython.core.display import HTML, Image
css_file = 'style.css'
HTML(open(css_file, 'r').read())
```

<link href='http://fonts.googleapis.com/css?family=Alegreya+Sans:100,300,400,500,700,800,900,100italic,300italic,400italic,500italic,700italic,800italic,900italic' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Arvo:400,700,400italic' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=PT+Mono' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Shadows+Into+Light' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Philosopher:400,700,400italic,700italic' rel='stylesheet' type='text/css'>

```python
#import numpy as np
from sympy import init_printing, Matrix, symbols
#import matplotlib.pyplot as plt
#import seaborn as sns
#from IPython.display import Image
from warnings import filterwarnings

init_printing(use_latex = 'mathjax')
%matplotlib inline
filterwarnings('ignore')
```

# Solving homogeneous systems
# Pivot variables
# Special solutions

* We are trying to solve a system of linear equations
* For homogeneous systems the right-hand side is the zero vector
* Consider the example below

```python
A = Matrix([[1, 2, 2, 2], [2, 4, 6, 8], [3, 6, 8, 10]])
A # A 3x4 matrix
```

$$\left[\begin{matrix}1 & 2 & 2 & 2\\2 & 4 & 6 & 8\\3 & 6 & 8 & 10\end{matrix}\right]$$

```python
x1, x2, x3, x4 = symbols('x1, x2, x3, x4')

x_vect = Matrix([x1, x2, x3, x4]) # A 4x1 matrix
x_vect
```

$$\left[\begin{matrix}x_{1}\\x_{2}\\x_{3}\\x_{4}\end{matrix}\right]$$

```python
b = Matrix([0, 0, 0])
b # A 3x1 matrix
```

$$\left[\begin{matrix}0\\0\\0\end{matrix}\right]$$

* The **x** column vector is a set of all the solutions to this homogeneous equation
* It forms the nullspace
* Note that the column vectors in A are not linearly independent

* Performing elementary row operations leaves us with the matrix below
* It has two pivots, which is termed **rank** 2

```python
A.rref() # rref being reduced row echelon form
```

$$\left ( \left[\begin{matrix}1 & 2 & 0 & -2\\0 & 0 & 1 & 2\\0 & 0 & 0 & 0\end{matrix}\right], \quad \left ( 0, \quad 2\right )\right )$$

* Which represents the following
$$ { x }_{ 1 }\begin{bmatrix} 1 \\ 0 \\ 0 \end{bmatrix}+{ x }_{ 2 }\begin{bmatrix} 2 \\ 0 \\ 0 \end{bmatrix}+{ x }_{ 3 }\begin{bmatrix} 0 \\ 1 \\ 0 \end{bmatrix}+{ x }_{ 4 }\begin{bmatrix} -2 \\ 2 \\ 0 \end{bmatrix}=\begin{bmatrix} 0 \\ 0 \\ 0 \end{bmatrix}\\ { x }_{ 1 }+2{ x }_{ 2 }+0{ x }_{ 3 }-2{ x }_{ 4 }=0\\ 0{ x }_{ 1 }+0{ x }_{ 2 }+{ x }_{ 3 }+2{ x }_{ 4 }=0\\ { x }_{ 1 }+0{ x }_{ 2 }+0{ x }_{ 3 }+0{ x }_{ 4 }=0 $$

* We are free set a value for *x*4, let's sat *t*
$$ { x }_{ 1 }+2{ x }_{ 2 }+0{ x }_{ 3 }-2{ x }_{ 4 }=0\\ 0{ x }_{ 1 }+0{ x }_{ 2 }+{ x }_{ 3 }+2t=0\\ { x }_{ 1 }+0{ x }_{ 2 }+0{ x }_{ 3 }+0{ x }_{ 4 }=0\\ \therefore \quad { x }_{ 3 }=-2t $$

* We will have to make *x*2 equal to another variable, say *s*
$$ { x }_{ 1 }+2s+0{ x }_{ 3 }-2t=0 $$
$$ \therefore \quad {x}_{1}=2t-2s $$

* This results in the following, which is the complete nullspace and has dimension 2
$$    \begin{bmatrix} { x }_{ 1 } \\ { x }_{ 2 } \\ { x }_{ 3 } \\ { x }_{ 4 } \end{bmatrix}=\begin{bmatrix} -2s+2t \\ s \\ -2t \\ t \end{bmatrix}=\begin{bmatrix} -2s \\ s \\ 0 \\ 0 \end{bmatrix}+\begin{bmatrix} 2t \\ 0 \\ -2t \\ t \end{bmatrix}=s\begin{bmatrix} -2 \\ 1 \\ 0 \\ 0 \end{bmatrix}+t\begin{bmatrix} 2 \\ 0 \\ -2 \\ 1 \end{bmatrix} $$
* From the above, we clearly have two vectors in the solution and we can take constant multiples of these to fill up our solution space (our nullspace)

* We can easily calculate how many free variables we will have by subtracting the number of pivots (rank) from the number of variables (*x*) in **x**
* Here we have 4 - 2 = 2

#### Example problem

* Calculate **x** for the transpose of A above

#### Solution

```python
A_trans = A.transpose() # Creating a new matrix called A_trans and giving it the value of the inverse of A
A_trans
```

$$\left[\begin{matrix}1 & 2 & 3\\2 & 4 & 6\\2 & 6 & 8\\2 & 8 & 10\end{matrix}\right]$$

```python
A_trans.rref() # In reduced row echelon form this would be the following matrix
```

$$\left ( \left[\begin{matrix}1 & 0 & 1\\0 & 1 & 1\\0 & 0 & 0\\0 & 0 & 0\end{matrix}\right], \quad \left ( 0, \quad 1\right )\right )$$

* Remember this is 4 equations in 3 unknowns, i.e.
$$ { x }_{ 1 }\begin{bmatrix} 1 \\ 0 \\ 0 \\ 0 \end{bmatrix}+{ x }_{ 2 }\begin{bmatrix} 0 \\ 1 \\ 0 \\ 0 \end{bmatrix}+{ x }_{ 3 }\begin{bmatrix} 1 \\ 1 \\ 0 \\ 0 \end{bmatrix}=\begin{bmatrix} 0 \\ 0 \\ 0 \\ 0 \end{bmatrix}\\ { x }_{ 1 }+0{ x }_{ 2 }+{ x }_{ 3 }=0\\ 0{ x }_{ 1 }+{ x }_{ 2 }+{ x }_{ 3 }=0\\ 0{ x }_{ 1 }+0{ x }_{ 2 }+0{ x }_{ 3 }=0\\ 0{ x }_{ 1 }+0{ x }_{ 2 }+0{ x }_{ 3 }=0 $$

* It seems we are free to choose a value for *x*3
* Let's make is *t*
$$ t\begin{bmatrix} 1 \\ 0 \\ 0 \\ 0 \end{bmatrix}-t\begin{bmatrix} 0 \\ 1 \\ 0 \\ 0 \end{bmatrix}+t\begin{bmatrix} 1 \\ 1 \\ 0 \\ 0 \end{bmatrix}=\begin{bmatrix} 0 \\ 0 \\ 0 \\ 0 \end{bmatrix}\\ { x }_{ 3 }=t\\ { x }_{ 1 }+0{ x }_{ 2 }+t=0\\ 0{ x }_{ 1 }+{ x }_{ 2 }+t=0\\ \therefore \quad { x }_{ 2 }=-t\\ \therefore \quad { x }_{ 1 }=-t\\ \begin{bmatrix} { x }_{ 1 } \\ { x }_{ 2 } \\ { x }_{ 3 } \end{bmatrix}=\begin{bmatrix} t \\ -t \\ t \end{bmatrix}=t\begin{bmatrix} 1 \\ -1 \\ 1 \end{bmatrix} $$

* We had *n* = 3 unknowns and *r* (rank) = 2 pivots
* The solution set (nullspace) will thus have 1 variable (*t*) (3-2=1)

* The third column is the sum of the first two, so only 2 columns are linearly independent
* We thus expect 2 pivots and can predict the nullspace to have only 1 variable (i.e. it is one-dimensional)

```python

```


```python
import numpy as np
import dapy.filters as filters
from dapy.models import NettoGimenoMendesModel
import matplotlib.pyplot as plt
%matplotlib inline
plt.style.use('seaborn-white')
plt.rcParams['figure.dpi'] = 100
```

## Model

One-dimensional stochastic dynamical system due to Netto et al. [1] with state dynamics defined by discrete time map

\begin{equation}
  x_{t+1} = \alpha x_t + \beta \frac{x_t}{1 + z_t^2} + \gamma \cos(\delta t) + \sigma_x u_t
\end{equation}

with $u_t \sim \mathcal{N}(0, 1) ~\forall t$ and $x_0 \sim \mathcal{N}(m, s^2)$.

Observed process defined by

\begin{equation}
  y_{t} = \epsilon x_t^2 + \sigma_y v_t
\end{equation}

with $v_t \sim \mathcal{N}(0, 1)$.

Standard parameter values assumed here are $\alpha = 0.5$, $\beta = 25$, $\gamma = 8$, $\delta = 1.2$, $\epsilon = 0.05$, $m=10$, $s=5$, $\sigma_x^2 = 1$, $\sigma_y^2 = 1$ and $T = 200$ simulated time steps.

### References

  1. M. L. A. Netto, L. Gimeno, and M. J. Mendes. A new spline algorithm for non-linear filtering of discrete time systems. *Proceedings of the 7th Triennial World Congress*, 1979.

```python
model_params = {
    'initial_state_mean': 10.,
    'initial_state_std': 5.,
    'state_noise_std': 1.,
    'observation_noise_std': 1.,
    'alpha': 0.5,
    'beta': 25.,
    'gamma': 8,
    'delta': 1.2,
    'epsilon': 0.05,
}
model = NettoGimenoMendesModel(**model_params)
```

## Generate data from model

```python
num_observation_time = 100
observation_time_indices = np.arange(num_observation_time)
seed = 20171027
rng = np.random.default_rng(seed)
state_sequence, observation_sequence = model.sample_state_and_observation_sequences(
    rng, observation_time_indices)
```

    Sampling: 100%

    100/100 [00:00<00:00, 21597.18time-steps/s]

```python
fig, ax = plt.subplots(figsize=(12, 4))
ax.plot(observation_time_indices, state_sequence)
ax.plot(observation_time_indices, observation_sequence, '.')
ax.set_xlabel('Time index $t$')
ax.set_ylabel('State')
_ = ax.set_xlim(0, num_observation_time - 1)
ax.legend(['$x_t$', '$y_t$'], ncol=4)
fig.tight_layout()
```

## Infer state from observations

```python
def plot_results(results, z_reference=None, plot_traces=True, plot_region=False,
                 trace_skip=1, trace_alpha=0.25):
    fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(12, 4))
    ax.plot(results['z_mean_seq'][:, 0], 'g-', lw=1)
    if plot_region:
        ax.fill_between(
            np.arange(n_steps),
            results['z_mean_seq'][:, 0] - 3 * results['z_std_seq'][:, 0],
            results['z_mean_seq'][:, 0] + 3 * results['z_std_seq'][:, 0],
            alpha=0.25, color='g'
        )
    if plot_traces:
        ax.plot(results['z_particles_seq'][:, ::trace_skip, 0],
                'r-', lw=0.25, alpha=trace_alpha)
    if z_reference is not None:
        ax.plot(z_reference[:, 0], 'k--')
    ax.set_ylabel('State $z$')
    ax.set_xlabel('Time index $t$')
    fig.tight_layout()
    return fig, ax

def plot_results(results, observation_time_indices, state_sequence=None,
                 plot_particles=True, plot_region=False,
                 particle_skip=5, trace_alpha=0.25):
    fig, ax = plt.subplots(sharex=True, figsize=(12, 4))
    ax.plot(results['state_mean_sequence'][:, 0], 'g-', lw=1, label='Est. mean')
    if plot_region:
        ax.fill_between(
            observation_time_indices,
            results['state_mean_sequence'][:, 0] - 3 * results['state_std_sequence'][:, 0],
            results['state_mean_sequence'][:, 0] + 3 * results['state_std_sequence'][:, 0],
            alpha=0.25, color='g', label='Est. mean ± 3 standard deviation'
        )
    if plot_particles:
        lines = ax.plot(
            observation_time_indices, results['state_particles_sequence'][:, ::particle_skip, 0],
            'r-', lw=0.25, alpha=trace_alpha)
        lines[0].set_label('Particles')
    if state_sequence is not None:
        ax.plot(observation_time_indices, state_sequence[:, 0], 'k--', label='Truth')
    ax.set_ylabel('$x_t$')
    ax.legend(loc='upper center', ncol=4)
    ax.set_xlabel('Time index $t$')
    fig.tight_layout()
    return fig, ax
```

### Ensemble Kalman filter (perturbed observations)

```python
enkf = filters.EnsembleKalmanFilter()
```

```python
results_enkf = enkf.filter(
    model, observation_sequence, observation_time_indices,
    num_particle=500, rng=rng, return_particles=True)
```

    Filtering: 100%

    100/100 [00:00<00:00, 427.09time-steps/s]

```python
fig, axes = plot_results(results_enkf, observation_time_indices, state_sequence)
```

### Ensemble Trasform Kalman filter (deterministic square root)

```python
etkf = filters.EnsembleTransformKalmanFilter()
```

```python
results_etkf = etkf.filter(
    model, observation_sequence, observation_time_indices,
    num_particle=500, rng=rng, return_particles=True)
```

    Filtering: 100%

    100/100 [00:00<00:00, 143.24time-steps/s]

```python
fig, axes = plot_results(results_etkf, observation_time_indices, state_sequence)
```

### Bootstrap particle filter

```python
bspf = filters.BootstrapParticleFilter()
```

```python
results_bspf = bspf.filter(
    model, observation_sequence, observation_time_indices,
    num_particle=500, rng=rng, return_particles=True)
```

    Filtering: 100%

    100/100 [00:00<00:00, 3062.47time-steps/s]

```python
fig, axes = plot_results(results_bspf, observation_time_indices, state_sequence)
```

### Ensemble transform particle filter

```python
etpf = filters.EnsembleTransformParticleFilter()
```

```python
results_etpf = etpf.filter(
    model, observation_sequence, observation_time_indices,
    num_particle=500, rng=rng, return_particles=True)
```

    Filtering: 100%

    100/100 [00:02<00:00, 41.33time-steps/s]

```python
fig, axes = plot_results(results_etpf, observation_time_indices, state_sequence)
```


# Why Neural Networks?

## Before NN
Unless the specific steps that computer needs to follow are known the computer cannot solve a problem
this restricts the problem solving capability of conventional computers to problems that we already understand and know how to solve

## After NN
-With Neural Networks computers can do things that we don't exactly know how to do.
-Neural Networks learn by example. They cannot be programmed to perform a specific task.

The term Neural Network exist since 1943 but at that time we don't have enough of data to train.

# Motivation behind NN

The building block of neural network is the neuron. An artificial neuron works much the same way the biological one does

NN are basecally computing systems inspired by the biological neural networks

# Descrption of NN

 A NN is constituted of Layers and each layer contains units.

 The First Layer is the inputs layer and the last one is the output layer.

 #### What is an unit?

 Unit, also called node or neuron is the basic building of Neural Networks. It has the following properties:

 * A unit is a function that takes as inputs a vectors and returns a scalar.

 * Each unit is characterized by its Weights $W$ and bias term $b$.

 * The output of a unit can be described by:

 \begin{equation}
 f\left( \sum_{i=1}^{n}x_{i}w_{i}+b\right),
 \end{equation}

 Where $f$ corresponds to the activation function.

# Example

An example of NN is given by the following figure:

We can notice from the figure above that the Network contains three layers denoted $L_{1}$ and $L_{2}$ and $L_{3}$, where $L_{1}$ and $L_{3}$ are respectively the inputs and  outputs Layer. $L_{2}$ is the hidden Layer.

* For $L_{2}$, we have five neurons, the inputs of those neurons are $x_{i}, i=1,\cdots,4$ and its activation function are given as follows:

  $a^{(2)}_{1}    =f\left(W^{(1)}_{11}x_{1}+W^{(1)}_{12}x_{2}+W^{(1)}_{13}x_{3}+W^{(1)}_{14}x_{4}+b^{(1)}_{1}\right).$

     $a^{(2)}_{2} =f\left(W^{(1)}_{21}x_{1}+W^{(1)}_{22}x_{2}+W^{(1)}_{23}x_{3}+W^{(1)}_{24}x_{4}+b^{(1)}_{2}\right).$

 $\vdots$

  $a^{(2)}_{5} =f\left(W^{(1)}_{51}x_{1}+W^{(1)}_{52}x_{2}+W^{(1)}_{53}x_{3}+W^{(1)}_{54}x_{4}+b^{(1)}_{5}\right).$

* For the output Layer, one has the following activation function:

$a^{(3)}_{1} = f\left(W^{(2)}_{11}a^{(2)}_{1}+ W^{(2)}_{12}a^{(2)}_{2}+\cdots +W^{(2)}_{15}a^{(2)}_{5}+b^{(2)}_{1}\right).$

For a more general case, I mean a NN with many hiddens layers the general formular for the activation function is given by:

$a_{i}^{(\ell+1)}  = f\left(\sum_{j=1}^{{s_{\ell}}}W_{ij}^{(\ell)} a_{j}^{(\ell)}+b_{i}^{(\ell)}\right).$

Let us set $z^{(\ell)}_{i}$, the total weighted sum of inputs to units $i$ of the layer $\ell$
such that:

$a^{(\ell)}_{i} = f\left(z^{(\ell)}_{i} \right).$

Note: $W^{(\ell)}_{ij}$ represents the connection between the $j-th$ unit of the layer $\ell$ and the $i-th$ unit of the layer $\ell+1$.

$b^{\ell}_{i}$ denotes the bias term of the unit $i$ of the layer $\ell+1$.

$a^{(\ell)}_{i}$, the activation function of unit $i$ of the layer $\ell.$

# Back Propagation

#### Error Terms

In the NN, the errors terms is given as follows:

\begin{equation}
J(W,b) = \frac{1}{2}\sum_{i=1}^{N}J(W,b;x^{i},y^{i})+ \frac{\lambda}{2}\sum_{\ell=1}^{n_{\ell}}\sum_{i}^{s_{\ell}}\sum_{j}^{s_{\ell+1}} \left(W^{(\ell)}_{ji}\right)^{2},
\end{equation}

Where:

$J(W,b;x,y)=\frac{1}{N}\sum_{i=1}^{N}||y_{i}-h_{W,b}(x^{(i)})||^{2} $

and $\left(W^{(\ell)}_{ji}\right)^{2}$ is the regularization term called the weight decay. It helps to decrease the magnitude of the weight function, hence to avoid Overfitting.

$\lambda $ is the weight decay parameter

#### Intuition behind the Backpropagation

* we  have a training dataset $D=\{x_{i},y_{i}\}$

* we perform the forward pass, that means we compute all the activation function through all the networks including the one of the output layer.

* Then, we compute the errors term $\delta_{i}^{(\ell)}$ that measures how much each node $i$ of layer $\ell$ was responsible for any errors in the output.

The backpropagation is an algorithm used in order to take in account the error brought by every node during the forward pass. it helps us to penalized the weight hence to avoid Overfitting as we mentioned before.

# How NN work?

### Perceptron (artificial neuron)
we will have multiples inputs $ x_{1}, x_{2}, \dots x_{n} $ and their corresponding weights $ w_{1},w_{2}, \dots w_{n}$ and we calculate the sum of the inputs $ s = \sum x_{i} w_{i}$ , then we pass it throught an activation function $Y$ and get the outputs

There are two modes in perceptron that is Training Mode and Using Mode

### Activation function
the activation function of a node defines the output of that node given an input or set of inputs. Most popular types of Activation functions
##### 1- Sigmoid or Logistic
  It is a activation function of form $ f(x) = 1 / 1 + \exp(-x)$ Its Range is between 0 and 1. It is a S — shaped curve. It is easy to understand and apply but it has major reasons which have made it fall out of popularity:
  - Vanishing gradient problem
  - Secondly , its output isn’t zero centered. It makes the gradient updates go too far in different directions. 0 < output < 1, and it makes optimization harder.
  - Sigmoids saturate and kill gradients.
  -Sigmoids have slow convergence.

##### 2- Tanh — Hyperbolic tangent
  It’s mathamatical formula is $ f(x) = 1 - \exp(-2x) / 1 + \exp(-2x)$ Now it’s output is zero centered because its range in between -1 to 1 i.e -1 < output < 1 . Hence optimization is easier in this method hence in practice it is always preferred over Sigmoid function . But still it suffers from Vanishing gradient problem.
 #### 3-ReLu -Rectified linear units
   It has become very popular in the past couple of years. It was recently proved that it had 6 times improvement in convergence from Tanh function. It’s just $ R(x) = max(0,x) i.e if x<0, R(x) = 0 and if x>0, R(x) = 0 so R(x) = x$ Hence as seeing the mathamatical form of this function we can see that it is very simple and efficinent . A lot of times in Machine learning and computer science we notice that most simple and consistent techniques and methods are only preferred and are best. Hence it avoids and rectifies vanishing gradient problem . Almost all deep learning Models use ReLu nowadays. But its limitation is that it should only be used within Hidden layers of a Neural Network Model. Hence for output layers we should use a Softmax function for a Classification problem to compute the probabilites for the classes , and for a regression problem it should simply use a linear function.

```python

```

## Multilayer perceptron

As you know our brain is made up of millions of neurons, so a NN is really just a composition of perceptrons, connected in different ways and operating on different activation functions

### Types of NN

#### 1-  Feedforward Neural Network – Artificial Neuron:
This neural network is one of the simplest form of ANN, where the data or the input travels in one direction. The data passes through the input nodes and exit on the output nodes. This neural network may or may not have the hidden layers. In simple words, it has a front propagated wave and no back propagation by using a classifying activation function usually.
Below is a Single layer feed forward network. Here, the sum of the products of inputs and weights are calculated and fed to the output. The output is considered if it is above a certain value i.e threshold(usually 0) and the neuron fires with an activated output (usually 1) and if it does not fire, the deactivated value is emitted (usually -1).

Application of Feed forward neural networks are found in computer vision and speech recognition where classifying the target classes are complicated. These kind of Neural Networks are responsive to noisy data and easy to maintain.
#### 2-  Radial basis function Neural Network:
Radial basic functions consider the distance of a point with respect to the center. RBF functions have two layers, first where the features are combined with the Radial Basis Function in the inner layer and then the output of these features are taken into consideration while computing the same output in the next time-step which is basically a memory.
Below is a diagram which represents the distance calculating from the center to a point in the plane similar to a radius of the circle. Here, the distance measure used in euclidean, other distance measures can also be used. The model depends on the maximum reach or the radius of the circle in classifying the points into different categories. If the point is in or around the radius, the likelihood of the new point begin classified into that class is high. There can be a transition while changing from one region to another and this can be controlled by the beta function.

This neural network has been applied in Power Restoration Systems. Power systems have increased in size and complexity. Both factors increase the risk of major power outages. After a blackout, power needs to be restored as quickly and reliably as possible. This paper how RBFnn has been implemented in this domain.
#### 3-Kohonen Self Organizing Neural Network
The objective of a Kohonen map is to input vectors of arbitrary dimension to discrete map comprised of neurons. The map needs to me trained to create its own organization of the training data. It comprises of either one or two dimensions. When training the map the location of the neuron remains constant but the weights differ depending on the value. This self organization process has different parts, in the first phase every neuron value is initialized with a small weight and the input vector. In the second phase, the neuron closest to the point is the ‘winning neuron’ and the neurons connected to the winning neuron will also move towards the point like in the graphic below. The distance between the point and the neurons is calculated by the euclidean distance, the neuron with the least distance wins. Through the iterations, all the points are clustered and each neuron represents each kind of cluster. This is the gist behind the organization of Kohonen Neural Network.

Kohonen Neural Network is used to recognize patterns in the data. Its application can be found in medical analysis to cluster data into different categories.
#### 4- Recurrent Neural Network(RNN) – Long Short Term Memory:
The Recurrent Neural Network works on the principle of saving the output of a layer and feeding this back to the input to help in predicting the outcome of the layer.
Here, the first layer is formed similar to the feed forward neural network with the product of the sum of the weights and the features. The recurrent neural network process starts once this is computed, this means that from one time step to the next each neuron will remember some information it had in the previous time-step. This makes each neuron act like a memory cell in performing computations. In this process, we need to let the neural network to work on the front propagation and remember what information it needs for later use. Here, if the prediction is wrong we use the learning rate or error correction to make small changes so that it will gradually work towards making the right prediction during the back propagation. This is how a basic Recurrent Neural Network looks like

The application of Recurrent Neural Networks can be found in text to speech(TTS) conversion models.
#### 5- Convolutional Neural Network:
Convolutional neural networks are similar to feed forward neural networks , where the neurons have learn-able weights and biases. Its application have been in signal and image processing which takes over OpenCV in field of computer vision.
Below is a representation of a ConvNet, in this neural network, the input features are taken in batch wise like a filter. This will help the network to remember the images in parts and can compute the operations. These computations involve conversion of the image from RGB or HSI scale to Gray-scale. Once we have this, the changes in the pixel value will help detecting the edges and images can be classified into different categories.

ConvNet are applied in techniques like signal processing and image classification techniques. Computer vision techniques are dominated by convolutional neural networks because of their accuracy in image classification.
#### 6- Modular Neural Network:
Modular Neural Networks have a collection of different networks working independently and contributing towards the output. Each neural network has a set of inputs which are unique compared to other networks constructing and performing sub-tasks. These networks do not interact or signal each other in accomplishing the tasks. The advantage of a modular neural network is that it breakdowns a large computational process into smaller components decreasing the complexity. This breakdown will help in decreasing the number of connections and negates the interaction of these network with each other, which in turn will increase the computation speed. However, the processing time will depend on the number of neurons and their involvement in computing the results.
Below is a visual representation,

Modular Neural Networks (MNNs) is a rapidly growing field in artificial Neural Networks research. This paper surveys the different motivations for creating MNNs: biological, psychological, hardware, and computational. Then, the general stages of MNN design are outlined and surveyed as well, viz., task decomposition techniques, learning schemes and multi-module decision-making strategies.

### Training a NN

The most common deep learning algorithm for supervised training of the multi-layer perceptrons is known as backpropagation. In it, after the weighted sum of inputs and passing through the activation function we propagate backwards and update the weights to reduce the error (desired output-model output).

```python

```

```python
import torch
import torchvision.datasets as dsets
import torch.nn as nn
from torch.nn import functional as F
import torchvision.transforms as transforms
import torch.optim as optim
import matplotlib.pyplot as plt
from torch.autograd import Variable
```

```python
class Model(nn.Module):
    def __init__(self, input_dim):
        super(Model, self).__init__()
        self.layer1 = nn.Linear(input_dim,50)
        self.layer2 = nn.Linear(50, 20)
        self.layer3 = nn.Linear(20, 3)

    def forward(self, x):
        x = F.relu(self.layer1(x))
        x = F.relu(self.layer2(x))
        x = F.softmax(self.layer3(x)) # To check with the loss function
        return x
```

```python
features, labels = load_iris(return_X_y=True)
features_train,features_test, labels_train, labels_test = train_test_split(features, labels, random_state=42, shuffle=True)

```

```python
# Training
model = Model(features_train.shape[1])
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
loss_fn = nn.CrossEntropyLoss()
epochs = 10

def print_(loss):
    print ("The loss calculated: ", loss)

```

```python
# Not using dataloader
x_train, y_train = Variable(torch.from_numpy(features_train)).float(), Variable(torch.from_numpy(labels_train)).long()
for epoch in range(1, epochs+1):
    print ("Epoch #",epoch)
    y_pred = model(x_train)
    loss = loss_fn(y_pred, y_train)
    print_(loss.item())

    # Zero gradients
    optimizer.zero_grad()
    loss.backward() # Gradients
    optimizer.step() # Update

```

    Epoch # 1
    The loss calculated:  1.084611177444458
    Epoch # 2
    The loss calculated:  1.0589287281036377
    Epoch # 3
    The loss calculated:  1.042871117591858
    Epoch # 4
    The loss calculated:  1.01982843875885
    Epoch # 5
    The loss calculated:  0.9865216016769409
    Epoch # 6
    The loss calculated:  0.9583816528320312
    Epoch # 7
    The loss calculated:  0.9344419836997986
    Epoch # 8
    The loss calculated:  0.9024511575698853
    Epoch # 9
    The loss calculated:  0.8794012665748596
    Epoch # 10
    The loss calculated:  0.8613719940185547

    /usr/lib/python3/dist-packages/ipykernel_launcher.py:11: UserWarning: Implicit dimension choice for softmax has been deprecated. Change the call to include dim=X as an argument.
      # This is added back by InteractiveShellApp.init_path()

```python
# Prediction
x_test = Variable(torch.from_numpy(features_test)).float()
pred = model(x_test)
```

    /usr/lib/python3/dist-packages/ipykernel_launcher.py:11: UserWarning: Implicit dimension choice for softmax has been deprecated. Change the call to include dim=X as an argument.
      # This is added back by InteractiveShellApp.init_path()

```python
pred = pred.detach().numpy()
pred
```

    array([[0.05697161, 0.29412127, 0.6489071 ],
           [0.91191626, 0.04918151, 0.03890222],
           [0.00211426, 0.14895074, 0.84893495],
           [0.05718697, 0.28539914, 0.6574139 ],
           [0.05055362, 0.28836423, 0.6610822 ],
           [0.8904781 , 0.06150251, 0.04801933],
           [0.17277575, 0.30878782, 0.51843643],
           [0.01842327, 0.22602433, 0.7555524 ],
           [0.03538903, 0.2949286 , 0.6696824 ],
           [0.12442167, 0.31731695, 0.5582614 ],
           [0.0239523 , 0.2356728 , 0.7403749 ],
           [0.87590134, 0.07037628, 0.05372234],
           [0.9233247 , 0.04335823, 0.03331704],
           [0.8766615 , 0.06977676, 0.05356177],
           [0.91224456, 0.04895632, 0.0387991 ],
           [0.06004994, 0.2714223 , 0.6685277 ],
           [0.00716032, 0.18816221, 0.8046775 ],
           [0.11222101, 0.32513776, 0.5626412 ],
           [0.05889581, 0.29232612, 0.648778  ],
           [0.00784537, 0.19586195, 0.79629266],
           [0.86162716, 0.07776302, 0.0606099 ],
           [0.0286808 , 0.25125998, 0.7200592 ],
           [0.87028795, 0.07244075, 0.05727138],
           [0.00852415, 0.20163555, 0.7898403 ],
           [0.01088909, 0.19102159, 0.79808927],
           [0.01404195, 0.21811026, 0.7678478 ],
           [0.00779451, 0.21146584, 0.78073967],
           [0.00705563, 0.18368259, 0.8092618 ],
           [0.8622539 , 0.07794169, 0.05980441],
           [0.8564604 , 0.08090509, 0.06263453],
           [0.91614676, 0.04696282, 0.03689051],
           [0.9427834 , 0.03169057, 0.02552599],
           [0.09865347, 0.29608843, 0.6052581 ],
           [0.8798515 , 0.06731425, 0.05283425],
           [0.88046765, 0.06737225, 0.05216017],
           [0.01643629, 0.24491987, 0.7386438 ],
           [0.0803118 , 0.2861273 , 0.63356084],
           [0.9047775 , 0.05353561, 0.04168688]], dtype=float32)

```python
print ("The accuracy is", accuracy_score(labels_test, np.argmax(pred, axis=1)))
```

    The accuracy is 0.7105263157894737

```python
# Checking for first value
np.argmax(model(x_test[0]).detach().numpy(), axis=0)
```

    /usr/lib/python3/dist-packages/ipykernel_launcher.py:11: UserWarning: Implicit dimension choice for softmax has been deprecated. Change the call to include dim=X as an argument.
      # This is added back by InteractiveShellApp.init_path()

    2

```python
labels_test[0]
```

    1

```python
torch.save(model, "iris-pytorch.pkl")
```

    /home/aims/.local/lib/python3.7/site-packages/torch/serialization.py:292: UserWarning: Couldn't retrieve source code for container of type Model. It won't be checked for correctness upon loading.
      "type " + obj.__name__ + ". It won't be checked "
    /home/aims/.local/lib/python3.7/site-packages/torch/serialization.py:292: UserWarning: Couldn't retrieve source code for container of type Linear. It won't be checked for correctness upon loading.
      "type " + obj.__name__ + ". It won't be checked "

```python
saved_model = torch.load("iris-pytorch.pkl")
```

```python
np.argmax(saved_model(x_test[0]).detach().numpy(), axis=0)
```

    /usr/lib/python3/dist-packages/ipykernel_launcher.py:11: UserWarning: Implicit dimension choice for softmax has been deprecated. Change the call to include dim=X as an argument.
      # This is added back by InteractiveShellApp.init_path()

    2

# Reference

- https://www.youtube.com/watch?v=fv6Qll3laUU
- https://www.kaggle.com/aaditkapoor1201/iris-classification-pytorch
- https://analyticsindiamag.com/6-types-of-artificial-neural-networks-currently-being-used-in-todays-technology

```python

```


# 1.1 Two-phase flow model

The water-oil displacement in a petroleum reservoir can be approximated
with the Buckley-Leverett (BL) model. The two-phase transport in a porous medium using the BL model can be described by **Equation 1**.

**Eq. 1**

\begin{equation}
\frac{\partial S_{w}}{\partial t}+\frac{q}{\varphi a} \frac{\partial}{\partial x}(f)=0, \text { in } \Omega \times[0, T]
\end{equation}

where

- **q** - total influx,

- **a** - core sample cross section area,

- **𝜑** - matrix porosity

\begin{equation}
S_{w}=S_{w}(x, t) \text {-denotes the water saturation}
\end{equation}

- **𝑓** - fractional flow function

**Eq. 2**

\begin{equation}
f=\frac{\boldsymbol{\kappa}_{r w}}{\boldsymbol{\kappa}_{r w}+\left(\frac{\mu_{w}}{\mu_{o}}\right) \boldsymbol{\kappa}_{r o}}
\end{equation}

where

\begin{equation}
\kappa_{r w}=\kappa_{r w}\left(S_{w}\right) \text { и } \kappa_{r o}=\kappa_{r o}\left(S_{w}\right) \text { - the water and oil phase relative permeabilities, respectively}
\end{equation}

\begin{equation}
\mu_{w} \text { , } \mu_{o} \text {- water and oil viscosities, respectively}
\end{equation}

### initial conditions:

**Eq. 3**
\begin{equation}
S_{w}(x, 0)=S_{w 0}
\end{equation}

### boundary conditions:

**Eq. 4**
\begin{equation}
S_{w}(0, t)=1.0-S_{o 0}
\end{equation}

the maximum of water is injected at a constant flow rate of **q**

```python
## Google Colab ations
#!pip install salib
#!pip install tqdm
#!git clone https://github.com/ABAZH666/project_uq_and_sa.git
#%cd project_uq_and_sa/

```

```python
# The initial code was provided by sonny-qa
# LINK [https://github.com/sonny-qa/buckley-leverett/blob/master/buckley-lev.ipynb]
import numpy as np
import pandas as pd
from scipy import interpolate
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon
import tqdm
import math

import warnings

```

```python
warnings.filterwarnings('ignore')
```

```python
class BuckleyLev():

    def __init__(self, porosity, viscosity_w, viscosity_o, diam, length, rate, initial_sw, residual_o, residual_w, kroe, krwe):
        self.params = {
            #OIL phase viscosity /// our case 𝜇o (non wetting phase)
            "viscosity_o": viscosity_o,

            #WATER phase viscosity /// our case 𝜇𝑤 (wetting phase)
            "viscosity_w": viscosity_w,

            #initial water sat /// our case Sw, in the initial conditions Sw=Sw0
            "initial_sw":initial_sw,

            #residual water saturation, Swirr (irreducible) /// our case 𝑆w0  - connate water saturation
            "residual_w":residual_w,

            #residual oil saturation, Sor /// our case 𝑆𝑜0
            "residual_o": residual_o,

            #water rel perm at water curve end point
            "krwe":krwe,

            #oil rel perm at oil curve end point
            "kroe": kroe,

            #dimless velocity results
            'vd_array':[],

            #porosity
            'poro':porosity,

            #water injection rate units TBD
            "inject_rate":rate,

            #cross sectional area units TBD
            "x-area": ((diam * 1.)/2 **2) * math.pi * length
        }

    def k_rw(self,sw):
        #water relative perm calculation for a given water saturation
        p = self.params["inject_rate"]
        return ((self.params['krwe']))*sw**p

    def k_rn(self,sw):
        #oil relative perm calculation for a given water saturation
        q = self.params["inject_rate"]
        return ((1.-self.params['kroe']*sw)**q)

    def fractional_flow(self,sw):
        #returns the fractional flow

        return 1./(1.+((self.k_rn(sw)/self.k_rw(sw))*(self.params["viscosity_w"]/self.params["viscosity_o"])))

    def fractional_flow_deriv(self,sw):
        #calculate derivative of fractional flow - dFw/dSw - Vsh

        f_deriv = (self.fractional_flow(sw+0.0001) - self.fractional_flow(sw))/0.0001

        return f_deriv

    def fractional_flow_2deriv(self,sw):
        f_2deriv = ((self.fractional_flow(sw+0.01))-2*(self.fractional_flow(sw))-self.fractional_flow(sw-0.01))/((0.01)**2)
        return f_2deriv

    def plot_fractional_flow_deriv(self, y):
        #plot the derivative dFw/dSw - Vsh vs Sw
        #y = np.linspace(self.params["residual_w"]+1e-3,(1-self.params["residual_o"]+1e-3),50)
        x = [self.fractional_flow_deriv(i) for i in y ]

        plt.plot(x,y)
        plt.title('Derivative of fractional flow curve')
        plt.xlabel('dfw/dSw')
        plt.ylabel('Sw')
        plt.show()

    def sw_at_shock_front(self, y):
        #first find range to search in by looking where 2nd derivative is negative (where curve turns)

        #sw_start = 1.-self.params['residual_o']
        #sw_end = self.params['residual_w']

        #for sw in np.arange(self.params['residual_w']+1.e-9, 1.-self.params['residual_o'],0.001):
        for sw in y:
            if (self.fractional_flow_2deriv(sw)< -1.e-2 and sw < y[0]):
                #i.e. we are below sw_start and the grad is negative - update upper limit
                sw_start = sw
            if (self.fractional_flow_2deriv(sw)< -1.e-2 and sw > y[-1]):
                # i.e. we are above sw_end and the gradient is negative - update lower limit
                sw_end = sw

        sw_at_front = 0.
        current_min = 1000.

        #find where finite difference approximation (almost) equals the continuous value of the function
        #for sw in np.arange(sw_start,sw_end, 0.0001):
        for sw in y:

            current_diff = abs(self.fractional_flow_deriv(sw)-self.fractional_flow(sw)/sw)
            #print( 'grad func',self.fractional_flow_deriv(sw),sw,self.fractional_flow(sw)/sw)
            #print('approx',self.fractional_flow(sw)/sw, sw)
            if current_diff < current_min:
                #print('sw at front',current_min, sw,current_diff)
                current_min = current_diff
                sw_at_front = sw

                current_min = current_diff

        return sw_at_front

    def plot_fractional_flow(self, x):
        #plot the sw vs fractional flow

        #x = np.linspace(self.params["residual_w"]+1e-3,(1-self.params["residual_o"]+1e-3),100)
        #x = np.linspace(self.params["residual_w"]+1e-3,(1),100)
        y = [self.fractional_flow(i) for i in x ]

        plt.plot(x,y)

        #sw_at_front = self.sw_at_shock_front()
        #plt.plot([sw_at_front],[self.fractional_flow(sw_at_front)],'ro')

        plt.title('Fractional flow as a function of water saturation')
        plt.xlabel('Sw')
        plt.ylabel('Fractional flow')
        #plt.ylim([0,0.5])
        plt.xlim([0,1])

        #add limiting fractional flow lines
        plt.hlines(y[len(y)-1],0,1,linestyles='dashed',lw=2, colors='0.4')
        plt.annotate('fw max: %.4f' % y[len(y)-1],xy=(0.08,0.95))

        plt.show()
        #print(y[len(y)-1])

    def displacement_plot(self, y):
        #using the shock front velocity, plot the displacement profile
        v_sh = self.sw_at_shock_front(y)

        #y = np.linspace(self.params["residual_w"]+1e-3,(1-self.params["residual_o"]+1e-3),50)
        x = [self.fractional_flow_deriv(i) for i in y if self.fractional_flow_deriv(i)>v_sh ]

        return x

    def rarefaction_plot(self, x):
        # it should calculate the shock velocity and the corresponding saturation which this occurs an

        #create dx window of increasing size for calculating welge
        #x = np.linspace(self.params["residual_w"]+1e-3,(1-self.params["residual_o"]+1e-3),50)

        maximum, sw_shock = 0,0
        grads = []

        # to do - add flag to allow BL_time function to calcuate sw values from supplied Xd values
        ## calculate welge gradient at each point until we find the max = shock velocity

        for swi in x:
            grad = self.fractional_flow(swi)/(swi-self.params["residual_w"]+1e-3)
            # vd = xd/td
            grads.append(grad)

            if grad>maximum:
                sw_shock = swi
                maximum = grad

        #find the position of the shock velocity in the grads array and slice to this point for the rarefaction
        #plus one to include the actual shock velocity. Reverse to get the values in the right order
        rarefaction = (grads[:([i for i,j in enumerate(grads) if j == maximum][0]+1)])
        rarefaction.sort(reverse=True)

        y = x[:(len(rarefaction))]

        #save calculated grads to instance
        print(rarefaction[0], rarefaction, y)
        for v in rarefaction:
            self.params['vd_array'].append(v)

        #plot rarefaction part
        plt.plot(rarefaction,y,'b',lw=2)

        #highlight vshock point
        #plt.plot(rarefaction[0],y[0],'ro')
        plt.plot(rarefaction[1],y[1],'ro')

        #add the part ahead of the shock front
        plt.vlines(rarefaction[0],y[0],self.params["initial_sw"],'b',lw=2)
        plt.hlines(self.params["initial_sw"],rarefaction[0],rarefaction[0]+1,'b',lw=2)

        #add saturation lines
        #plt.hlines(self.params["initial_sw"],0,rarefaction[0],linestyles='dashed',lw=2,colors='0.4')
        plt.hlines(y[1],0,rarefaction[1],linestyles='dashed',lw=2,colors='0.4')
        plt.hlines(y[0],0,rarefaction[0],linestyles='dashed',lw=2, colors='0.4')
        plt.vlines(rarefaction[0],-2,self.params["initial_sw"],linestyles='dashed',lw=2, colors='0.4')

        # add annotations
        plt.annotate('V shock',xy=(rarefaction[0]+0.02,y[0]+0.02))
        plt.annotate('Sw f',xy=(0+0.04,y[0]-0.05))
        plt.annotate('Sw i',xy=(0+0.04,y[1]-0.05))#self.params["initial_sw"]-0.05))

        #ad vdmin if the min velocity values is not zero
        if (rarefaction[len(rarefaction)-1]>0.e-22):
            plt.hlines(y[len(y)-1],0,rarefaction[len(rarefaction)-1],'b',lw=2)
            plt.plot(rarefaction[len(rarefaction)-1],y[len(y)-1],'ro')
            plt.annotate('V min',xy=((rarefaction[len(rarefaction)-1]+0.08),(y[len(y)-1])))
            plt.vlines(rarefaction[len(rarefaction)-1],y[len(y)-1],0,linestyles='dashed',lw=2, colors='0.4')
            plt.annotate('Sw c',xy=(0+0.04,1-self.params["residual_o"]+0.03))

        plt.xlabel('Dimensionless Velocity, Vd = xd/td')
        plt.ylabel('Saturation')

        plt.ylim([0,1])
        plt.xlim([-0.1,rarefaction[0]+1])
        plt.title('Saturation profile velocity')
        plt.show()

```

# Cherie

```python
class Cherie():

    def __init__(self , initial_sw, residual_w, residual_o, krwe, kroe, viscosity_o, viscosity_w):
        self.B = 1.
        self.M = 1.
        self.A = 1.
        self.L = 1.
        self.params = {

            #initial water sat /// our case Sw, in the initial conditions Sw=Sw0
            "initial_sw":initial_sw,

            #residual water saturation, Swirr (irreducible) /// our case 𝑆w0  - connate water saturation
            "residual_w":residual_w,

            #residual oil saturation, Sor /// our case 𝑆𝑜0
            "residual_o": residual_o,

            #water rel perm at water curve end point
            "krwe":krwe,

            #oil rel perm at oil curve end point
            "kroe": kroe,

            #OIL phase viscosity /// our case 𝜇o (non wetting phase)
            "viscosity_o": viscosity_o,

            #WATER phase viscosity /// our case 𝜇𝑤 (wetting phase)
            "viscosity_w": viscosity_w,

        }

    def calc_krw(self, sw):
        return self.params['krwe']*math.e**(-self.B*(((sw-self.params['residual_w']+0.00001)/(1-sw-self.params['residual_o']))**(-self.M)))

    def calc_kro(self, sw):
        return self.params['kroe']*math.e**(-self.A*(((sw-self.params['residual_w']+0.00001)/(1-sw-self.params['residual_o']))**(-self.L)))

    def fractional_flow(self,sw):
        #returns the fractional flow
        return self.calc_krw(sw)/(self.calc_krw(sw)+(self.params['viscosity_w'] / self.params['viscosity_o'])*self.calc_kro(sw)+0.0001)
        #return 1./(1.+((self.k_rn(sw)/self.k_rw(sw))*(self.params["viscosity_w"]/self.params["viscosity_o"])))

    def plot_fractional_flow(self, x):
        #plot the sw vs fractional flow

        #x = np.linspace(self.params["residual_w"]+1e-3,(1-self.params["residual_o"]+1e-3),100)
        #x = np.linspace(self.params["residual_w"]+1e-3,(1),100)
        y = [self.fractional_flow(i) for i in x ]

        plt.plot(x,y)

        #sw_at_front = self.sw_at_shock_front()
        #plt.plot([sw_at_front],[self.fractional_flow(sw_at_front)],'ro')

        plt.title('Fractional flow as a function of water saturation')
        plt.xlabel('Sw')
        plt.ylabel('Fractional flow')
        #plt.ylim([0,0.5])
        plt.xlim([0,1])

        #add limiting fractional flow lines
        plt.hlines(y[len(y)-1],0,1,linestyles='dashed',lw=2, colors='0.4')
        plt.annotate('fw max: %.4f' % y[len(y)-1],xy=(0.08,0.95))

        plt.show()
        #print(y[len(y)-1])

    def rarefaction_plot(self, x):
        # it should calculate the shock velocity and the corresponding saturation which this occurs an

        #create dx window of increasing size for calculating welge
        #x = np.linspace(self.params["residual_w"]+1e-3,(1-self.params["residual_o"]+1e-3),50)

        maximum, sw_shock = 0,0
        grads = []

        # to do - add flag to allow BL_time function to calcuate sw values from supplied Xd values
        ## calculate welge gradient at each point until we find the max = shock velocity

        for swi in x:
            grad = self.fractional_flow(swi)/(swi-self.params["residual_w"]+1e-3)
            # vd = xd/td
            grads.append(grad.real)
            #print(self.fractional_flow(swi), swi-self.params["residual_w"]+1e-3,  grad)

            if grad.real>maximum:
                sw_shock = swi
                maximum = grad.real
        #print(grads)
        #find the position of the shock velocity in the grads array and slice to this point for the rarefaction
        #plus one to include the actual shock velocity. Reverse to get the values in the right order
        #rarefaction = (grads[:([i for i,j in enumerate(grads) if j == maximum][0]+1)])
        #rarefaction = (grads[:([i for i,j in enumerate(grads)][0]+1)])
        rarefaction=grads
        rarefaction.sort(reverse=True)

        y = x[:(len(rarefaction))]
        #print(y)
        #print(rarefaction)

        #save calculated grads to instance
        #print(rarefaction[0], rarefaction, y)
        #for v in rarefaction:
        #    self.params['vd_array'].append(v)

        #plot rarefaction part
        plt.plot(rarefaction,y,'b',lw=2)

        #highlight vshock point
        #plt.plot(rarefaction[0],y[0],'ro')
        plt.plot(rarefaction[5],y[5],'ro')

        #add the part ahead of the shock front
        plt.vlines(rarefaction[0],y[0],self.params["initial_sw"],'b',lw=2)
        plt.hlines(self.params["initial_sw"],rarefaction[0],rarefaction[0]+1,'b',lw=2)

        #add saturation lines
        #plt.hlines(self.params["initial_sw"],0,rarefaction[0],linestyles='dashed',lw=2,colors='0.4')
        plt.hlines(y[5],0,rarefaction[5],linestyles='dashed',lw=2,colors='0.4')
        plt.hlines(y[0],0,rarefaction[0],linestyles='dashed',lw=2, colors='0.4')
        plt.vlines(rarefaction[0],-2,self.params["initial_sw"],linestyles='dashed',lw=2, colors='0.4')

        # add annotations
        plt.annotate('V shock',xy=(rarefaction[5]+0.02,y[5]+0.02))
        plt.annotate('Sw f',xy=(0+0.04,y[0]-0.05))
        plt.annotate('Sw i',xy=(0+0.04,y[1]-0.05))#self.params["initial_sw"]-0.05))

        #ad vdmin if the min velocity values is not zero
        if (rarefaction[len(rarefaction)-1]>0.e-22):
            plt.hlines(y[len(y)-1],0,rarefaction[len(rarefaction)-1],'b',lw=2)
            plt.plot(rarefaction[len(rarefaction)-1],y[len(y)-1],'ro')
            plt.annotate('V min',xy=((rarefaction[len(rarefaction)-1]+0.08),(y[len(y)-1])))
            plt.vlines(rarefaction[len(rarefaction)-1],y[len(y)-1],0,linestyles='dashed',lw=2, colors='0.4')
            plt.annotate('Sw c',xy=(0+0.04,1-self.params["residual_o"]+0.03))

        plt.xlabel('Dimensionless Velocity, Vd = xd/td')
        plt.ylabel('Saturation')

        plt.ylim([0,1])
        plt.xlim([0,rarefaction[0]+1])
        plt.title('Saturation profile velocity')
        plt.show()

    def sobol_target_function(self, x):
        # it should calculate the shock velocity and the corresponding saturation which this occurs an

        #create dx window of increasing size for calculating welge
        #x = np.linspace(self.params["residual_w"]+1e-3,(1-self.params["residual_o"]+1e-3),50)

        maximum, sw_shock = 0,0
        grads = []

        # to do - add flag to allow BL_time function to calcuate sw values from supplied Xd values
        ## calculate welge gradient at each point until we find the max = shock velocity

        for swi in x:
            grad = self.fractional_flow(swi)/(swi-self.params["residual_w"]+1e-3)
            grads.append(grad.real)

            if grad.real>maximum:
                sw_shock = swi
                maximum = grad.real

        rarefaction=grads
        rarefaction.sort(reverse=True)

        y = x[:(len(rarefaction))]
        return rarefaction[5], y[5]

```

```python
qi1 = pd.read_excel("./datasets/dataset1.xlsx")
swlist1=list(qi1['Sw'])
krw1=list(qi1['Krw'])
kro1=list(qi1['Kro'])
```

```python
ch2=Cherie(
                viscosity_w = 0.306,
                viscosity_o = 0.670,
                initial_sw = 0.2,
                residual_o = 0.26,
                residual_w = 0.2,
                kroe = 0.95,
                krwe = 0.3)
```

```python
qi2 = pd.read_excel("./datasets/dataset2.xlsx")
swlist2=list(qi2['Sw'])
krw2=list(qi2['Krw'])
kro2=list(qi2['Kro'])
```

```python
ch1=Cherie(
                viscosity_w = 0.65,
                viscosity_o = 1.25,

                initial_sw = 0.15,
                residual_o = 0.35,
                residual_w = 0.15,
                kroe = 0.65,
                krwe = 0.25)
```

```python
ch1.fractional_flow(swlist1[0])
```

    0.17897393575103684

```python
#вместо mcmc используется gridsearch + SALib library
```

```python
def get_error(ch, swlist, kro, krw):
    cur_error=0
    for i in range(len(swlist)):
        cur_krw = ch.calc_krw(swlist[i])
        cur_kro = ch.calc_kro(swlist[i])
        cur_error += np.abs(kro[i]-cur_kro)**2+np.abs(krw[i]-cur_krw)**2
    return cur_error

def find_params(ch, swlist, kro, krw, al, ar, ll, lr, bl, br, ml, mr):
    glob_error=10**10
    glob_params=[-1, -1, -1, -1]

    for A in range(al, ar):
        ch.A=A/100
        for L in range(ll , lr):
            ch.L=L/100
            for B in range(bl, br):
                ch.B=B/100
                for M in range(ml, mr):
                    ch.M=M/100
                    cur_error = get_error(ch, swlist, kro, krw)
                    if cur_error < glob_error:
                        glob_error = cur_error
                        glob_params = [ch.A, ch.L, ch.B, ch.M]

    print(glob_error, glob_params)
    return glob_params
```

```python
ch1.A, ch1.L, ch1.B, ch1.M=find_params(ch1,  swlist1, kro1, krw1,\
                                       147, 159, 60, 68, 283, 296, 64, 72)
```

    9.84548795563146 [1.47, 0.6, 2.83, 0.71]

```python
#ch1.plot_fractional_flow(swlist1)
```

```python
ch1.rarefaction_plot(swlist1)
```

```python
ch2.A, ch2.L, ch2.B, ch2.M=find_params(ch2,  swlist2, kro2, krw2, 191, 206, 42, 49, 186, 204, 80, 97)
```

    234478257.7869995 [2.03, 0.47, 1.86, 0.8]

```python
#ch2.plot_fractional_flow(swlist2)
```

```python
ch2.rarefaction_plot(swlist2)
```

# LET

```python
class LET():

    def __init__(self , initial_sw, residual_w, residual_o, krwe, kroe, viscosity_o, viscosity_w):
        self.Lw = 1.
        self.Lo = 1.
        self.Ew = 1.
        self.Eo = 1.
        self.Tw = 1.
        self.To = 1.
        self.params = {

            #initial water sat /// our case Sw, in the initial conditions Sw=Sw0
            "initial_sw":initial_sw,

            #residual water saturation, Swirr (irreducible) /// our case 𝑆w0  - connate water saturation
            "residual_w":residual_w,

            #residual oil saturation, Sor /// our case 𝑆𝑜0
            "residual_o": residual_o,

            #water rel perm at water curve end point
            "krwe":krwe,

            #oil rel perm at oil curve end point
            "kroe": kroe,

            #OIL phase viscosity /// our case 𝜇o (non wetting phase)
            "viscosity_o": viscosity_o,

            #WATER phase viscosity /// our case 𝜇𝑤 (wetting phase)
            "viscosity_w": viscosity_w,

        }

    def calc_krw(self, sw):
        #effective water saturation
        Swe = (sw-self.params['residual_w'])/(1-sw-self.params['residual_o'])
        return self.params['krwe']*((Swe**self.Lw)/(Swe**self.Lw + self.Ew*((1 - Swe)**self.Tw)))

    def calc_kro(self, sw):
        Swe = (sw-self.params['residual_w'])/(1-sw-self.params['residual_o'])
        #print(Swe, self.Lo, self.To)
        return self.params['kroe']*((1- Swe)**self.Lo)/(((1 - Swe)**self.Lo)+self.Eo*(Swe**self.To))

    def fractional_flow(self,sw):
        #returns the fractional flow
        return self.calc_krw(sw)/(self.calc_krw(sw)+(self.params['viscosity_w'] / self.params['viscosity_o'])*self.calc_kro(sw)+0.0001)
        #return 1./(1.+((self.k_rn(sw)/self.k_rw(sw))*(self.params["viscosity_w"]/self.params["viscosity_o"])))

    def plot_fractional_flow(self, x):
        #plot the sw vs fractional flow

        #x = np.linspace(self.params["residual_w"]+1e-3,(1-self.params["residual_o"]+1e-3),100)
        #x = np.linspace(self.params["residual_w"]+1e-3,(1),100)
        y = [self.fractional_flow(i) for i in x ]

        plt.plot(x,y)

        #sw_at_front = self.sw_at_shock_front()
        #plt.plot([sw_at_front],[self.fractional_flow(sw_at_front)],'ro')

        plt.title('Fractional flow as a function of water saturation')
        plt.xlabel('Sw')
        plt.ylabel('Fractional flow')
        #plt.ylim([0,0.5])
        plt.xlim([0,1])

        #add limiting fractional flow lines
        plt.hlines(y[len(y)-1],0,1,linestyles='dashed',lw=2, colors='0.4')
        plt.annotate('fw max: %.4f' % y[len(y)-1],xy=(0.08,0.95))

        plt.show()
        #print(y[len(y)-1])

    def rarefaction_plot(self, x):
        # it should calculate the shock velocity and the corresponding saturation which this occurs an

        #create dx window of increasing size for calculating welge
        #x = np.linspace(self.params["residual_w"]+1e-3,(1-self.params["residual_o"]+1e-3),50)

        maximum, sw_shock = 0,0
        grads = []

        # to do - add flag to allow BL_time function to calcuate sw values from supplied Xd values
        ## calculate welge gradient at each point until we find the max = shock velocity

        for swi in x:
            grad = self.fractional_flow(swi)/(swi-self.params["residual_w"]+1e-3)
            # vd = xd/td
            grads.append(grad.real)
            #print(self.fractional_flow(swi), swi-self.params["residual_w"]+1e-3,  grad)

            if grad.real>maximum:
                sw_shock = swi
                maximum = grad.real
        #print(grads)
        #find the position of the shock velocity in the grads array and slice to this point for the rarefaction
        #plus one to include the actual shock velocity. Reverse to get the values in the right order
        #rarefaction = (grads[:([i for i,j in enumerate(grads) if j == maximum][0]+1)])
        #rarefaction = (grads[:([i for i,j in enumerate(grads)][0]+1)])
        rarefaction=grads
        rarefaction.sort(reverse=True)

        y = x[:(len(rarefaction))]
        #print(y)
        #print(rarefaction)

        #save calculated grads to instance
        #print(rarefaction[0], rarefaction, y)
        #for v in rarefaction:
        #    self.params['vd_array'].append(v)

        #plot rarefaction part
        plt.plot(rarefaction,y,'b',lw=2)

        #highlight vshock point
        #plt.plot(rarefaction[0],y[0],'ro')
        plt.plot(rarefaction[1],y[1],'ro')

        #add the part ahead of the shock front
        plt.vlines(rarefaction[0],y[0],self.params["initial_sw"],'b',lw=2)
        plt.hlines(self.params["initial_sw"],rarefaction[0],rarefaction[0]+1,'b',lw=2)

        #add saturation lines
        #plt.hlines(self.params["initial_sw"],0,rarefaction[0],linestyles='dashed',lw=2,colors='0.4')
        plt.hlines(y[5],0,rarefaction[5],linestyles='dashed',lw=2,colors='0.4')
        plt.hlines(y[0],0,rarefaction[0],linestyles='dashed',lw=2, colors='0.4')
        plt.vlines(rarefaction[0],-2,self.params["initial_sw"],linestyles='dashed',lw=2, colors='0.4')

        # add annotations
        plt.annotate('V shock',xy=(rarefaction[1]+0.02,y[1]+0.02))
        plt.annotate('Sw f',xy=(0+0.04,y[0]-0.05))
        plt.annotate('Sw i',xy=(0+0.04,y[1]-0.05))#self.params["initial_sw"]-0.05))

        #ad vdmin if the min velocity values is not zero
        if (rarefaction[len(rarefaction)-1]>0.e-22):
            plt.hlines(y[len(y)-1],0,rarefaction[len(rarefaction)-1],'b',lw=2)
            plt.plot(rarefaction[len(rarefaction)-1],y[len(y)-1],'ro')
            plt.annotate('V min',xy=((rarefaction[len(rarefaction)-1]+0.08),(y[len(y)-1])))
            plt.vlines(rarefaction[len(rarefaction)-1],y[len(y)-1],0,linestyles='dashed',lw=2, colors='0.4')
            plt.annotate('Sw c',xy=(0+0.04,1-self.params["residual_o"]+0.03))

        plt.xlabel('Dimensionless Velocity, Vd = xd/td')
        plt.ylabel('Saturation')

        plt.ylim([0,1])
        plt.xlim([0,rarefaction[0]+1])
        plt.title('Saturation profile velocity')
        plt.show()

    def sobol_target_function(self, x ):
        # it should calculate the shock velocity and the corresponding saturation which this occurs an

        #create dx window of increasing size for calculating welge
        #x = np.linspace(self.params["residual_w"]+1e-3,(1-self.params["residual_o"]+1e-3),50)

        maximum, sw_shock = 0,0
        grads = []

        # to do - add flag to allow BL_time function to calcuate sw values from supplied Xd values
        ## calculate welge gradient at each point until we find the max = shock velocity

        for swi in x:
            grad = self.fractional_flow(swi)/(swi-self.params["residual_w"]+1e-3)
            # vd = xd/td
            grads.append(grad.real)

            if grad.real>maximum:
                sw_shock = swi
                maximum = grad.real

        rarefaction=grads
        rarefaction.sort(reverse=True)

        y = x[:(len(rarefaction))]

        return rarefaction[1], y[1]

```

```python
qi1 = pd.read_excel("./datasets/dataset1.xlsx")
swlist1=list(qi1['Sw'])
krw1=list(qi1['Krw'])
kro1=list(qi1['Kro'])
```

```python
let2=LET(
                viscosity_w = 0.306,
                viscosity_o = 0.670,
                initial_sw = 0.2,
                residual_o = 0.26,
                residual_w = 0.2,
                kroe = 0.95,
                krwe = 0.3)
```

```python
qi2 = pd.read_excel("./datasets/dataset2.xlsx")
swlist2=list(qi2['Sw'])
krw2=list(qi2['Krw'])
kro2=list(qi2['Kro'])
```

```python
let1=LET(
                viscosity_w = 0.65,
                viscosity_o = 1.25,

                initial_sw = 0.15,
                residual_o = 0.35,
                residual_w = 0.15,
                kroe = 0.65,
                krwe = 0.25)
```

```python
let1.fractional_flow(swlist1[0])
```

    0.08460523198754613

```python
#вместо mcmc используется gridsearch
```

```python
def get_error(let, swlist, kro, krw):
    cur_error=0
    for i in range(len(swlist)):
        cur_krw = let.calc_krw(swlist[i])
        cur_kro = let.calc_kro(swlist[i])
        cur_error += np.abs(kro[i]-cur_kro)**2+np.abs(krw[i]-cur_krw)**2
    return cur_error
```

```python
def find_params(let, swlist, kro, krw, lwl, lwr, lol, lor, ewl, ewr, eol,eor, twl, twr, tol, tor, step):
    glob_error=10**10
    glob_params=[-1, -1, -1, -1, -1, -1]

    for Lw in range(lwl, lwr, step):
        let.Lw=Lw/100
        for Lo in range(lol, lor, step):
            let.Lo=Lo/100
            for Ew in range(ewl, ewr, step):
                let.Ew=Ew/100
                for Eo in range(eol, eor, step):
                    let.Eo=Eo/100
                    for Tw in range(twl, twr, step):
                        let.Tw=Tw/100
                        for To in range(tol, tor, step):
                            let.To=To/100

                            cur_error = get_error(let, swlist, kro, krw)
                            if cur_error < glob_error:
                                glob_error = cur_error
                                glob_params = \
                                        [let.Lw,
                                        let.Lo,
                                        let.Ew,
                                        let.Eo,
                                        let.Tw,
                                        let.To]

    print(glob_error, glob_params)
    return glob_params
```

```python
let1.Lw, let1.Lo, let1.Ew,let1.Eo,let1.Tw,let1.To=find_params(let1,  swlist1, kro1, krw1, 282, 362, 98, 136, 232, 621, 643, 1538, 66, 120, 216, 295, 30)
```

    2.916079580522968 [2.82, 0.98, 5.92, 6.43, 0.66, 2.76]

```python
#ch1.plot_fractional_flow(swlist1)
```

```python
let1.rarefaction_plot(swlist1)
```

```python
let2.Lw, let2.Lo, let2.Ew,let2.Eo,let2.Tw,let2.To=find_params(let1,  swlist1, kro1, krw1, 334, 457, 120, 172, 24, 143, 310, 778, 1, 105, 66, 133, 30)
```

    4.373321395539807 [3.34, 1.2, 1.14, 3.7, 0.01, 1.26]

```python
#ch2.plot_fractional_flow(swlist2)
```

```python
let2.rarefaction_plot(swlist2)
```

# BL

```python
qi1 = pd.read_excel("./datasets/dataset1.xlsx")
swlist1=list(qi1['Sw'])
krw1=list(qi1['Krw'])
kro1=list(qi1['Kro'])
```

```python
qi2 = pd.read_excel("./datasets/dataset2.xlsx")
swlist2=list(qi2['Sw'])
```

```python
#Dataset 2
bl2 = BuckleyLev(
                porosity = 0.2062,
                viscosity_w = 0.65,
                viscosity_o = 1.25,
                diam = 0.05,
                length = 1.22,
                rate = 30,
                initial_sw = 0.15,
                residual_o = 0.35,
                residual_w = 0.15,
                kroe = 0.65,
                krwe = 0.25
                )
```

```python
bl1 = BuckleyLev(
                porosity = 0.29,
                viscosity_w = 0.306,
                viscosity_o = 0.670,
                diam = 0.037,
                length = 0.1174,
                rate = 5,
                initial_sw = 0.2,
                residual_o = 0.26,
                residual_w = 0.2,
                kroe = 0.95,
                krwe = 0.3
                )
```

```python
bl2.plot_fractional_flow_deriv(list(qi1['Sw'][:-1]))
```

```python

```

```python
bl2.plot_fractional_flow(swlist1)
```

```python
bl2.sw_at_shock_front(swlist1)
```

    0.2

```python
bl1.rarefaction_plot(swlist1)
```

```python
bl1.sw_at_shock_front(swlist1)
```

    0.2

```python
bl2.rarefaction_plot(swlist2)
```

```python
bl2.sw_at_shock_front(swlist2)
```

    0.15

# 1.2 Relative permeability models

### Two widely used **relative permeability models** were considered for the Buckley-Leverett (Eq. 1):

###  a) The Chierici’s relative permeability model

**Eq. 5**
\begin{equation}
\kappa_{r w}=\kappa_{w}^{0} e^{-B\left(\frac{S_{w}-S_{w 0}}{1-S_{w}-S_{o 0}}\right)^{-M}}
\quad \kappa_{r o}=\kappa_{o}^{0} e^{-A\left(\frac{S_{w}-S_{w 0}}{1-S_{w}-S_{o 0}}\right)^{L}}
\end{equation}

where

- **A**, **L**, **B** and **M** - parameters for the relative permeabilities

###  b) The LET relative permeability model

**Eq. 6**
\begin{equation}
\kappa_{r w}=\kappa_{w}^{0} \frac{S_{w e}^{L_{w}}}{S_{w e}^{L_{w}}+E_{w}\left(1-S_{w e}\right)^{T_{w}}} \quad \kappa_{r o}=\kappa_{o}^{0} \frac{\left(1-S_{w e}\right)^{L_{o}}}{\left(1-S_{w e}\right)^{L_{o}}+E_{o} S_{w e}^{T_{o}}}
\end{equation}

where

- **Lw**, **Ew**, **Tw**, **Lo**, **Eo**, and **To** - parameters of the model  (**w** - water phase and **o** - oil phase)

**Swe** - effective water saturation calculated as:

**Eq. 7**

\begin{equation}
S_{w e}=\frac{S_{w}-S_{w 0}}{1-S_{w 0}-S_{o 0}}
\end{equation}

where

- **Sw0** - connate water saturation,

- **So0** the residual oil saturation,

- **𝜅0o** - oil relative permeability at the connate water saturation,

- **𝜅0𝑤** - water relative permeability at the residual oil saturation

# 1.3 Datasets

!разобраться откуда какие данные!

# 1.4 Input parameters and quantities of interest

To perform UQ and SA studies we must define a set of input parameters θ and relevant **quantities of interest (QoIs)**:

### a) For Chierici’s model the input parameters:

\begin{equation}
θ_{c}={({B; M; A; L})}
\end{equation}

### b) For LET model the input parameters:

\begin{equation}
θ_{c}={({L_{w},E_{w},T_{w},L_{o},E_{o},T_{o}})}
\end{equation}

### The following **QoIs** were considered in this study:

**Scpt** - relative permeability crossing point,

**Eq. 8**

\begin{equation}
\kappa_{r w}\left(S_{c p t}\right)-\kappa_{r o}\left(S_{c p t}\right)=0
\end{equation}

**S𝑓** -shock front saturation,

**Eq. 9**

\begin{equation}
\frac{\partial f\left(S_{f}\right)}{\partial S_{w}}\left(S_{f}-S_{w 0}\right)-f\left(S_{f}\right)=0
\end{equation}

**V(t)** - displaced oil at a fixed time

**Eq. 10**

\begin{equation}
V(t)=a \int_{0}^{t} q\left(1-f\left(S_{w}(L, \tau)\right)\right) d \tau
\end{equation}

where

- **f(Sw(L,𝜏)** - fractional flow function evaluated at any time instant **τ** and at the position **x=L** which, corresponds to the size of the core sample.

The datasets and the simulations performed in this work focus on core-scale experiments. QoIs used in this work were chosen considering their relevance to both core-scale and field-scale experiments. For instance, the crossing point saturation **Scpt** and the shock front saturation **S𝑓** are essential indicators of the performance for both mature or new reservoirs.

# 1.5 Parameter and distribution estimation

For each relative permeability model (Chierici and LET) we performed a parameter estimation based on the presented datasets using the **Markov Chain Monte Carlo (MCMC) method**.

**(MCMC implemented in emcee library, implemented in the lmfit package, was used to obtain an initial guess for the relative permeability model parameters required for the MCMC).**

### Assuming θ as the relative permeability model parameters and D as the dataset, MCMC tries to estimate:

**Eq. 11**

\begin{equation}
\mathbb{P}(\boldsymbol{\theta} \mid D)=\frac{\mathbb{P}(\boldsymbol{D} \mid \boldsymbol{\theta}) \mathbb{P}(\boldsymbol{\theta})}{\mathbb{P}(D)}
\end{equation}

where

- **ℙ(𝜽)** - prior knowledge about the input parameters **θ** - joint probability distribution,

- **ℙ(𝑫∣𝜽)** - likelihood function,

- **ℙ(𝐷)** - evidence that the data was generated by this model.

In **Eq. 11** - likelihood function:

**Eq. 12**

\begin{equation}
\ln (\mathbb{P}(D \mid \boldsymbol{\theta}))=-0.5 \sum_{i}^{N_{d}}\left[\left(\frac{D^{i}-y^{i}(\boldsymbol{\theta})}{\sigma^{i}}\right)^{2}+2 \log \left(\sigma^{i}\right)\right]
\end{equation}

where

- **Nd** - size of the dataset,

- **𝜎𝑖** represents the error values,

- **𝑦𝑖(𝜽)** - entry of the vector representing the relative permeability model evaluated at the sample parameters **θ**.

The MCMC method requires the definition of a prior distribution - **flat prior distributions:**

**Eq. 13**

\begin{equation}
\ln (\mathbb{P}(\boldsymbol{\theta}))=\{\text { 0if all entries of } \boldsymbol{\theta} \in \mathbb{R}>0
\end{equation}

-∞ elsewhere.

# 2.1 Uncertainty quantification

# 3.1 Sensitivity analysis

```python
def estimate_sobol_LET(let, swlist, kro, krw, Lw, Lo, Ew, Eo, Tw, To):
    let.Lw=Lw
    let.Lo=Lo
    let.Ew=Ew
    let.Eo=Eo
    let.Tw=Tw
    let.To=To

    for i in range(len(swlist)):
        cur_krw = let.calc_krw(swlist[i])
        cur_kro = let.calc_kro(swlist[i])

#     bl2.sw_at_shock_front(swlist)
#     x, y = let.rarefaction_plot(swlist)
    x, y = let.sobol_target_function(swlist)
    return (x,y)
```

```python
from SALib.sample import saltelli
from SALib.analyze import sobol
from SALib.plotting.bar import plot as barplot

problem_LET = {
    'num_vars': 6,
    'names': ['Lw', 'Lo', 'Ew', 'Eo', 'Tw', 'To'],
    'bounds': [[3.34, 4.57],
             [1.20, 1.72],
             [0.24, 1.43],
             [3.10, 7.78],
             [0.01, 1.05],
             [0.66, 1.33]]
}

param_values_LET = saltelli.sample(problem_LET, 1000)

```

```python

```

```python
res_x = []
res_y= []
for z in tqdm.tqdm(range(param_values_LET.shape[0])):
    x, y = estimate_sobol_LET(let1,  swlist1, kro1, krw1, *param_values_LET[z,:])
    res_x.append(x)
```

```python
Si = sobol.analyze(problem_LET, np.array(res_x))
```

```python
Si_dict = dict(Si)
sensitivity_indices = ['S1', 'ST', 'S1_conf', 'ST_conf']
Si_df = pd.DataFrame()
for SI in sensitivity_indices:
    Si_df = Si_df.append(pd.Series(Si_dict[SI]), ignore_index=True)

Si_df = Si_df.T
Si_df.columns = sensitivity_indices
Si_df['Parameters'] =problem_LET['names']
Si_df.set_index('Parameters', inplace=True)
Si_df
```

<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>S1</th>
      <th>ST</th>
      <th>S1_conf</th>
      <th>ST_conf</th>
    </tr>
    <tr>
      <th>Parameters</th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>Lw</th>
      <td>0.016460</td>
      <td>0.043505</td>
      <td>0.018543</td>
      <td>0.006587</td>
    </tr>
    <tr>
      <th>Lo</th>
      <td>0.151927</td>
      <td>0.194765</td>
      <td>0.039795</td>
      <td>0.022718</td>
    </tr>
    <tr>
      <th>Ew</th>
      <td>0.310756</td>
      <td>0.381869</td>
      <td>0.053734</td>
      <td>0.040227</td>
    </tr>
    <tr>
      <th>Eo</th>
      <td>0.251280</td>
      <td>0.311204</td>
      <td>0.043966</td>
      <td>0.028110</td>
    </tr>
    <tr>
      <th>Tw</th>
      <td>0.186132</td>
      <td>0.247474</td>
      <td>0.047750</td>
      <td>0.029945</td>
    </tr>
    <tr>
      <th>To</th>
      <td>0.008843</td>
      <td>0.035918</td>
      <td>0.016579</td>
      <td>0.006223</td>
    </tr>
  </tbody>
</table>
</div>

```python
plt.style.use('ggplot')
from SALib.plotting.bar import plot as barplot
barplot(Si_df)
plt.title('SA of LET model parameters')
plt.legend(['$S_{main}$', '$S_{total}$'])
plt.show()
```

## SA of Chersie model

```python
def estimate_sobol_CHERSIE(ch, swlist,kro, krw, A, L, B, M):
    ch.A=A
    ch.L=L
    ch.B=B
    ch.M=M

    for i in range(len(swlist)):
        cur_krw = ch.calc_krw(swlist[i])
        cur_kro = ch.calc_kro(swlist[i])

    x, y = ch.sobol_target_function(swlist)
    return (x,y)

```

```python
problem_Chersie = {
    'num_vars': 4,
    'names': ['A', 'B', 'L', 'M'],
    'bounds': [[1.47, 2.06],
             [1.86, 2.96],
             [0.42, 0.68],
             [0.64, 0.97]]
}

param_values_Chersie = saltelli.sample(problem_Chersie, 10000)
```

```python
import tqdm
```

```python
res_x = []
res_y= []
for z in tqdm.tqdm(range(param_values_Chersie.shape[0])):
    x, y = estimate_sobol_CHERSIE(ch1,  swlist1, kro1, krw1, *param_values_Chersie[z,:])
    res_x.append(x)
```

    100%|██████████| 100000/100000 [00:37<00:00, 2699.10it/s]

```python
Si = sobol.analyze(problem_Chersie, np.array(res_x))
```

```python
Si_dict = dict(Si)
sensitivity_indices = ['S1', 'ST', 'S1_conf', 'ST_conf']
Si_df = pd.DataFrame()
for SI in sensitivity_indices:
    Si_df = Si_df.append(pd.Series(Si_dict[SI]), ignore_index=True)

Si_df = Si_df.T
Si_df.columns = sensitivity_indices
Si_df['Parameters'] =problem_Chersie['names']
Si_df.set_index('Parameters', inplace=True)
Si_df
# Si_df.to_excel()
```

<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>S1</th>
      <th>ST</th>
      <th>S1_conf</th>
      <th>ST_conf</th>
    </tr>
    <tr>
      <th>Parameters</th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>A</th>
      <td>0.076036</td>
      <td>0.314274</td>
      <td>0.016861</td>
      <td>0.028607</td>
    </tr>
    <tr>
      <th>B</th>
      <td>0.349778</td>
      <td>0.595866</td>
      <td>0.034394</td>
      <td>0.041734</td>
    </tr>
    <tr>
      <th>L</th>
      <td>0.235089</td>
      <td>0.258976</td>
      <td>0.016202</td>
      <td>0.014685</td>
    </tr>
    <tr>
      <th>M</th>
      <td>0.085365</td>
      <td>0.098098</td>
      <td>0.007766</td>
      <td>0.006036</td>
    </tr>
  </tbody>
</table>
</div>

```python

plt.style.use('ggplot')
from SALib.plotting.bar import plot as barplot
barplot(Si_df)
plt.title('SA of Chersie model parameters')
plt.legend(['$S_{main}$', '$S_{total}$'])
# plt.savefig('./plots/SA_Chersie_model.png', dpi=300)
plt.show()
```

```python

```