# Final Project:                                          McGill - COMP767 - FALL 2019
Student name: Antonios Valkanas (ID: 260672034)

## Ising Model Empirical Testing of IMCMC

#### Ising model
The Ising model is fully expressed using a symmetric adjacency matrix $A$ (```adj```):
\begin{align}
  p(x) = \frac{1}{Z} \exp(\sum_{i} A_{i,i}x_i + \sum_{i,j < i} x_i A_{i,j}x_j) \quad x_i \in \{-1,+1\}
\end{align}
In words, the diagonal of $A$ represents the local field and interactions are captured by the remaining non-zero elements of $A$/

First we define MCMC and IMCMC

```python
import numpy as np
import matplotlib.pylab as plt
import networkx as nx
np.random.seed(12345)
import timeit
import warnings
import matplotlib.cbook
warnings.filterwarnings("ignore",category=matplotlib.cbook.mplDeprecation)
from utils import tensor_mult, draw_graph, logistic
from inference import Inference, CliqueTreeInference
from IPython.core.debugger import set_trace
import seaborn as sn
sn.set_context('notebook')
%matplotlib inline
from matplotlib import colors
from random import randint
```

```python
def initial_state(dim):
    '''
    Create a dim by dim array of numbers chosen randomly from -1 or 1.
    '''
    random_state = np.random.choice([-1,1], size = (dim,dim))

    return random_state

def plot_grid(grid, title=None):
    '''
    Plot grid with -1 being black and 1 being white with an option to add a title.
    '''
    # Create discrete colormap
    cmap = colors.ListedColormap(['black', 'white'])
    bounds = [-2,0,2]
    norm = colors.BoundaryNorm(bounds, cmap.N)

    # Make plot
    fig, ax = plt.subplots()
    ax.imshow(grid, cmap = cmap, norm=norm)
    ax.set_title(title)
```

```python
class MCMC():
    """
    implements MCMC for an Ising model
    for  p(x) \propto exp(+\sum_{i,j<i} x_i x_j adj[i,j] + \sum_{i} x_i adj[i,i]) for  x_i \in {-1,+1}
    """
    def __init__(self,
                 adj,
                 H,# self interaction
                 J,# cross interaction
                 beta, # inverse of temperature parameter
                 verbosity=0,
                 max_iters=200000, # max number of iterations
                 informed_start=None,
    ):
        self.max_iters = max_iters
        self.verbosity = verbosity
        self.beta = beta
        self.state = None
        self.marginals = None
        self.magnetizations = None
        self.converged = False
        self.informed_start = informed_start
        self.H = H
        self.J = J
        self.adj = adj
        self._initialize()

    def _initialize(self):
        if self.informed_start is None:
            self.state = initial_state(int(np.sqrt(self.H.shape[0])))
            if self.verbosity > 1:
                print('Random start',self.state)
        else:
            self.state = self.informed_start
            if self.verbosity > 1:
                print('Random start',self.state)
        self.converged = False
        self.marginals = np.zeros(self.H.shape[0])
        self.magnetizations = np.zeros(self.max_iters)

    def _inference(self):
        if self.verbosity > 0:
            print("Running MCMC", flush = True)

        def get_local_energy(grid, x, y):
            '''
            Find the energy change due to a spin flip of one node.
            '''
            n = grid.shape[0]
            delta_energy = self.H[n*x+y] # Self interaction

            if (x != 0): # Above
                delta_energy += self.J * grid[x-1,y]

            if (x != n-1): # Below
                delta_energy += self.J * grid[x+1,y]

            if (y != 0): # Left
                delta_energy += self.J * grid[x,y-1]

            if (y != n-1): # Right
                delta_energy += self.J * grid[x,y+1]

            delta_energy = 2 * (grid[x,y]) * delta_energy

            return delta_energy

        # Initialization
        n = self.state.shape[0]
        adj = self.adj.copy()
        t = 1
        num_rejects = 0
        magnetizations = []
        state_hist = []
        state_hist.append(self.state.flatten())
        magnetizations.append(np.sum(self.state.flatten()))

        while t < self.max_iters:
            t+=1
            i = randint(0,n-1)
            j = randint(0,n-1)

            # Spin the flip
            temp_state = self.state.copy()
            temp_state[i,j] *= -1

            # Calculate Proposal's effect on Hamiltonian
            delta_energy = get_local_energy(self.state, i,j)
            if delta_energy < 0 or np.random.random() < np.exp(-delta_energy*self.beta):
                self.state = temp_state.copy()
                state_hist.append(self.state.flatten())
                magnetizations.append(np.sum(self.state.flatten()))
            else:
                num_rejects += 1
                state_hist.append(self.state.flatten())
                magnetizations.append(np.sum(self.state.flatten()))

        new_marginals_p = (np.sum(np.array(state_hist), axis = 0) + t)/(2*t)
        self.marginals = new_marginals_p
        self.magnetizations = np.array(magnetizations)

        self.marginals = new_marginals_p
        if self.verbosity > 0:
            print("Done, proportion of rejects:", num_rejects/t, flush = True)

    def get_marginal(self, target):
        if not self.converged:
            self._inference()
            self.converged = True
        mrg = (self.marginals)[target]
        return mrg

    def get_magnetizations(self):
        if not self.converged:
            self._inference()
            self.converged = True
        return self.magnetizations

    def get_partition(self):
        if not self.converged:
            self._inference()
            self.converged = True
        mrg = self.marginals
        return np.exp(2*self.marginals - 1).sum()
```

```python
size = 8
j = 0.05
h = np.random.uniform(-1,1,size*size)
T = 1

tree = nx.grid_2d_graph(size,size)
n = nx.number_of_nodes(tree)
x_0 = initial_state(size)
mask = np.array(nx.adjacency_matrix(tree).todense(), dtype = np.float64)

# create interactions over the edges
row,col = np.diag_indices(mask.shape[0])
mask *= 0.15
mask[row,col] = h
adj = mask

# make it symmetric
zero_inds = np.tril_indices_from(adj,-1)
adj[zero_inds] = 0
adj += adj.T.copy()

# do inference
# to change method change following line to LBP or MF
true_margs = CliqueTreeInference(adj).get_marginal(list(range(adj.shape[0])))
mcmc_margs = MCMC(adj = adj, H = h, J = j, beta = 1, verbosity=0).get_marginal(list(range(adj.shape[0]))).flatten()

print('mean error rand:', np.mean(np.abs(true_margs - mcmc_margs)))

true_mag = np.sum(2*true_margs - 1)
length = 1000
line = [true_mag for i in range(length)]

map_start = (np.rint(true_margs)*2-1).reshape(int(np.sqrt(adj.shape[0])),int(np.sqrt(adj.shape[0])))

mcmc_margs_2 = MCMC(adj = adj, H = h, J = j, beta = 1, verbosity=1, informed_start = map_start).get_marginal(list(range(adj.shape[0]))).flatten()
print('mean error informed:', np.mean(np.abs(true_margs - mcmc_margs_2)))

magn = MCMC(adj = adj, H = h, J = j, beta = 1/T, verbosity=1).get_magnetizations()
magn_2 = MCMC(adj = adj, H = h, J = j, beta = 1/T, verbosity=1, informed_start = map_start).get_magnetizations()
```

    mean error rand: 0.10952670049487662
    Running MCMC
    Done, proportion of rejects: 0.420865
    mean error informed: 0.10533477866729021
    Running MCMC
    Done, proportion of rejects: 0.42211
    Running MCMC
    Done, proportion of rejects: 0.42107

```python
mean_line = [true_mag for i in range(magn.shape[0])]
mean_line_2 = [magn.mean() for i in range(magn.shape[0])]

plt.plot(magn)
plt.plot(mean_line)
plt.plot(mean_line_2)
plt.show()

plt.plot(magn[:length])
plt.plot(magn_2[:length])
plt.plot(line[:length])
plt.show()
# plt.plot(magn[-500:])
# sn.distplot(magn)
```

```python
length = 1000
_line = [true_mag for i in range(length)]
from matplotlib.pyplot import figure
figure(num=None, figsize=(8, 6), dpi=400)
plt.plot(magn[:length], label = 'Standard MCMC')
plt.plot(magn_2[:length], label = 'Informed MCMC')
plt.plot(_line[:length],label = 'True Magnetization')
plt.xlabel('Step Number')
plt.ylabel('Magnetization')
plt.title('Ising Lattice Magnetization Estimation')
plt.legend()
# plt.savefig('8x8Ising1000Steps')
plt.show()
```

```python
plt.plot(magn)
plt.plot(magn_2)
plt.plot(mean_line)
plt.show()
```

```python
print(np.mean(magn), np.std(magn))
print(np.mean(magn_2), np.std(magn_2))
print(true_mag)
```

    1.1715 7.4374153944767665
    1.11533 7.582222562751637
    2.7119460005716687

```python
def autocorr(x):
    result = np.correlate(x, x, mode='full')
    return result
```

```python
print(np.abs(autocorr(magn)).mean())
print(np.abs(autocorr(magn_2)).mean())
```

    156356.25245063112
    146038.98514746287

```python
s1 = (autocorr(magn))
s2 = (autocorr(magn_2))
```

```python
s1 = s1/s1.max()
s2 = s2/s2.max()
```

```python
figure(num=None, figsize=(8, 6), dpi=400)
plt.xlabel('Time Lag')
plt.ylabel('Autocorrelation')
plt.title('Normalized Autocorrelation vs. Time Lag')
plt.acorr(magn-np.mean(magn), normed=True, usevlines=False, maxlags=1000, label='MCMC samples')
plt.acorr(magn_2-np.mean(magn_2), normed=True, usevlines=False, maxlags=1000, label='IMCMC samples')
plt.legend()
# plt.savefig('autocorr')
plt.show()
```

```python
def estimate_ess(n, cor):
    return n/(1+np.sum(cor)/2)
```

```python
estimate_ess(300000,s1)
```

    16.635834119645303

```python
estimate_ess(300000,s2)
```

    16.931290747226285

```python
def run_experiment_ess(size = 8, num_exp = 10, maxiters = 300000):
    ess_mcmc = []
    ess_imcmc = []
    for z in range(num_exp):
#         print(z)
        j = 0.15
        h = np.random.uniform(-1,1,size*size)
#         print(np.abs(h).mean(), np.std(h))
        T = 1

        tree = nx.grid_2d_graph(size,size)
        n = nx.number_of_nodes(tree)
        x_0 = initial_state(size)
        mask = np.array(nx.adjacency_matrix(tree).todense(), dtype = np.float64)

        # create interactions over the edges
        row,col = np.diag_indices(mask.shape[0])
        mask *= j
        mask[row,col] = h
        adj = mask

        # make it symmetric
        zero_inds = np.tril_indices_from(adj,-1)
        adj[zero_inds] = 0
        adj += adj.T.copy()

        # do inference
        # to check other method change the next line
        true_margs = CliqueTreeInference(adj).get_marginal(list(range(adj.shape[0])))

        true_mag = np.sum(2*true_margs - 1)

        map_start = (np.rint(true_margs)*2-1).reshape(int(np.sqrt(adj.shape[0])),int(np.sqrt(adj.shape[0])))

        magn = MCMC(adj = adj, H = h, J = j, beta = 1/T, max_iters=maxiters).get_magnetizations()
        magn_2 = MCMC(adj = adj, H = h, J = j, beta = 1/T, max_iters=maxiters, informed_start = map_start).get_magnetizations()

        s1 = (autocorr(magn))
        s2 = (autocorr(magn_2))
        s1 = s1/s1.max()
        s2 = s2/s2.max()

        ess_mcmc.append(estimate_ess(maxiters,s1))
        ess_imcmc.append(estimate_ess(maxiters,s2))
#         print('mcmc:',estimate_ess(maxiters,s1))
#         print('i-mcmc',estimate_ess(maxiters,s2))

    return ess_mcmc, ess_imcmc
```

```python
n = 10
runs = 1000
steps = 1000

ess_mcmc, ess_imcmc = run_experiment_ess(size = n, num_exp = runs, maxiters = steps)
ess_mcmc = np.array(ess_mcmc)
ess_imcmc = np.array(ess_imcmc)
# print((ess_imcmc - ess_mcmc)/ess_mcmc)
```

```python
print('i-mcmc/mcmc ratio (mean, std):', ((ess_imcmc-ess_mcmc)/ess_mcmc).mean(),  ((ess_imcmc-ess_mcmc)/ess_mcmc).std())
```

    i-mcmc/mcmc ratio (mean, std): 6.32245900761781 26.34558316808611

```python
ess_mcmc.mean()
```

    54.154506759515805

```python
ess_mcmc.std()
```

    150.63974805786432

```python
ess_imcmc.mean()
```

    58.813737247705646

```python
ess_imcmc.std()
```

    168.70810077935994

```python
def get_normalizing_constant(size = 8, num_exp = 10, maxiters = 10000):
    Z_mcmc = []
    Z_imcmc = []

    for z in range(num_exp):
        j = 0.15
        size = 16

        h = np.random.uniform(-1,1,size*size)
        #         print(np.abs(h).mean(), np.std(h))
        T = 256
        tree = nx.grid_2d_graph(size,size)
        n = nx.number_of_nodes(tree)
        x_0 = initial_state(size)
        mask = np.array(nx.adjacency_matrix(tree).todense(), dtype = np.float64)

        # create interactions over the edges
        row,col = np.diag_indices(mask.shape[0])
        mask *= j
        mask[row,col] = h
        adj = mask

        # make it symmetric
        zero_inds = np.tril_indices_from(adj,-1)
        adj[zero_inds] = 0
        adj += adj.T.copy()

        # do inference
        # To change method change the next line
        true_margs = CliqueTreeInference(adj).get_marginal(list(range(adj.shape[0])))

        true_mag = np.sum(2*true_margs - 1)

        map_start = (np.rint(true_margs)*2-1).reshape(int(np.sqrt(adj.shape[0])),int(np.sqrt(adj.shape[0])))

        Z_mcmc.append(MCMC(adj = adj, H = h, J = j, beta = 1/T, max_iters=maxiters).get_partition())
        Z_imcmc.append(MCMC(adj = adj, H = h, J = j, beta = 1/T, max_iters=10000, informed_start = map_start).get_partition())

    return (Z_imcmc, Z_mcmc)
```

```python
iZ, Z = get_normalizing_constant(size = 16, num_exp = 10, maxiters = 10000)
```

```python
iZ = np.array(iZ)
Z = np.array(Z)

print('Z (imcmc) (mean, std):',iZ.mean(),  iZ.std())
print('Z (mcmc) (mean, std):',Z.mean(),  Z.std())

plt.figure(figsize=(15,10))
plt.errorbar([1], iZ.mean(), yerr= iZ.std(), fmt='o', elinewidth=3)
plt.errorbar([2], Z.mean(), yerr= iZ.std(), fmt='o',elinewidth=3)
plt.errorbar([3], [255.21], yerr= [3.5], fmt='o',elinewidth=3)
plt.errorbar([4], [256.15], yerr= [0.35], fmt='o',elinewidth=3)
plt.axhline(y=256, linestyle='-', label = "Ground Truth")
plt.xticks([])
plt.yticks([251,252,253,254,255,256,257,258,259,260,261,262,263,264,265])
plt.ylabel("Z", fontsize=16)
plt.legend(["Ground Truth", "IMCMC", "MCMC", "PSMC - 64", "Twisted PSMC - 64"], fontsize=16)
plt.title("IMCMC vs. MCMC Ising Partition Function.", fontsize=18)
# plt.savefig('partition_function')
plt.show()
```

# The following code is adapted from Assignment 3

## Loopy Belief Propagation

The implementation of loopy-BP.
The BP message $m_{i \to j}$ for a pairwise MRF is the product of all incoming messages, and the relevant factors, marginalized over $x_i$. For the Ising model this is:
\begin{align}
m_{i \to j}(x_j) \propto (1-\alpha) m^{old}_{i \to j}(x_j) \, + \, \alpha \sum_{x_i \in \{-1,+1\}} \left ( \exp(x_i A_{i,i} + x_i A_{i,j} x_j) \prod_{k \in Nb(i)-j} m_{k \to i}(x_i) \right )
\end{align}
where $\alpha$ called the damping factor helps with the convergence. After convergence the marginals are:
\begin{align}
p(x_i) \propto \exp(x_i A_{i,i}) \prod_{j \in Nb(i)} m_{j \to i}(x_i)
\end{align}

```python
class LBPInference(Inference):
    """
    implements sum-product loopy belief propagation inference
    for  p(x) \propto exp(+\sum_{i,j<i} x_i x_j adj[i,j] + \sum_{i} x_i adj[i,i]) for  x_i \in {-1,+1}
    """
    def __init__(self,
                 adj,
                 verbosity=0,
                 max_iters=1000,  # max number of iterations
                 damping=.1,  # damping=0 means no damping
                 eps=1e-16,  # threshold for convergence
    ):
        Inference.__init__(self, adj, verbosity=verbosity)
        self.damping = damping
        self.max_iters = max_iters
        # (i,j)th element is the log-ratio of message i->j
        self.msg_new = None
        self.msg_old = None
        self.marginals = None
        self.converged = False
        self.mask = None  # zero-one mask for the adjacency (zero diagonals)
        self.eps = eps
        self._initialize()
        self.adj = adj

    def update_adjacency(self, new_adj):
        self.adj = new_adj
        self._initialize()

    def _initialize(self):
        n = self.adj.shape[0]
        self.msg_new = np.zeros((n , n))
        self.msg_old = np.zeros((n , n))
        self.marginals = np.zeros(n)
        mask = (self.adj != 0)
        np.fill_diagonal(mask, 0)
        self.mask = mask
        self.converged = False

    def _apply_damping(self, msg_new, msg_old):
        #convert to probability
        msg_new_p = logistic(msg_new)
        msg_old_p = logistic(msg_old)
        msg_p = msg_old_p * (self.damping) + msg_new_p * (1. - self.damping)
        # convert back to log-ratio
        msg = np.log(msg_p) - np.log(1. - msg_p)
        return msg

    def _inference(self):
        if self._verbosity > 0:
            print("running BP", flush = True)

        # >>>> YOUR CODE HERE >>>>>>>
        def apply_damping(msg_new, msg_old, damping):
            #convert to probability
            msg_new_p = logistic(msg_new)
            msg_old_p = logistic(msg_old)
            msg_p = msg_old_p * (damping) + msg_new_p * (1. - damping)
            # convert back to log-ratio
            msg = np.log(msg_p) - np.log(1. - msg_p)
            return msg

        # Initialization
        nonz = np.nonzero(self.adj)
        pairwise_set = set([(nonz[0][i], nonz[1][i]) for i in range(len(nonz[0]))])
        old_msg = np.zeros_like(self.adj)
        new_msg = np.ones_like(self.adj)
        adj = self.adj.copy()

        t = 0
        while t < self.max_iters and np.max(np.abs(new_msg - old_msg)) > self.eps:
            t += 1
            old_msg = new_msg.copy()
            for i, j in pairwise_set:
                if i != j :
                    prod_p = 1
                    prod_n = 1
                    for k in range(adj.shape[1]):
                        if k != j and k != i and self.adj[k,i] != 0:
                            prod_p *= logistic(old_msg[k,i])
                            prod_n *= (1-logistic(old_msg[k,i]))
                    new_msg[i,j] = np.exp(-adj[i,i] - adj[i,j]) * prod_n + np.exp(adj[i,i] + adj[i,j]) * prod_p
                    complement = np.exp(-adj[i,i] + adj[i,j]) * prod_n + np.exp(adj[i,i] - adj[i,j]) * prod_p
                    new_msg[i,j] = np.log(new_msg[i,j]) - np.log(complement)

            new_msg = apply_damping(new_msg, old_msg, self.damping)

        new_marginals_p = np.ones(adj.shape[0])
        new_marginals_n = np.ones(adj.shape[0])
        for i in range(adj.shape[0]):
            new_marginals_p[i] = np.exp(adj[i,i])
            new_marginals_n[i] = np.exp(-adj[i,i])
            for j in range(adj.shape[1]):
                if self.adj[j,i] != 0 and j != i:
                    new_marginals_p[i] *= logistic(new_msg[j,i])
                    new_marginals_n[i] *= (1 - logistic(new_msg[j,i]))

        new_marginals_p /= (new_marginals_p + new_marginals_n)
        # >>>> YOUR CODE HERE >>>>>>>

        self.marginals = new_marginals_p
        if self._verbosity > 0:
            print("finished after {} iterations!".format(t), flush = True)

    def get_marginal(self, target):
        if not self.converged:
            self._inference()
            self.converged = True
        mrg = (self.marginals)[target]
        return mrg
```

If the implementation above is correct, it should return exact marginals for the tree structure below. You can use this to debug your code (We are comparing the ```LBPInference``` marginals against the exact values returned by ```CliqueTreeInference```.)

```python
#build a tree
tree = nx.balanced_tree(3, 4)
n = nx.number_of_nodes(tree)
mask = np.array(nx.adjacency_matrix(tree).todense())
# create a random interaction over the edges
np.fill_diagonal(mask, 1.)
adj = mask * np.random.randn(n , n)
# make it symmetric
zero_inds = np.tril_indices_from(adj,-1)
adj[zero_inds] = 0
adj += adj.T.copy()
# do inference
true_margs = CliqueTreeInference(adj).get_marginal(list(range(adj.shape[0])))
bp_margs = LBPInference(adj, verbosity = 1).get_marginal(list(range(adj.shape[0])))

assert np.max(np.abs(true_margs - bp_margs)) < 1e-10, "LBP marginals are not exact"
```

    running BP
    finished after 31 iterations!

## Mean Field

The implementation of naive mean-field.

```python
class MFInference(Inference):
    """
    implements parallel update of naive mean-field for the Ising model
    mean-field is guaranteed to converge to a local optima, when updating
    one coordinate at a time. Here, since for efficiency/simplicity we update all coordinates simultaneously,
    we may need to use damping to help with convergence
    for  p(x) \propto exp(+\sum_{i,j<i} x_i x_j adj[i,j] + \sum_{i} x_i adj[i,i]) for  x_i \in {-1,+1}
    """

    def __init__(self,
                 adj,  # adjacency matrix for the Ising model
                 verbosity=0,
                 max_iters=1000,  # max number of iterations
                 eps=1e-16,  # threshold for convergence
                 damping=.5, # because we are using parallel updates, 0 means no damping
    ):
        Inference.__init__(self, adj, verbosity=verbosity)
        self.damping = damping
        self.max_iters = max_iters
        # (i,j)th element is the log-ratio of message i->j
        self.means = None
        self.converged = False
        self.adj_masked = None  # adjacency matrix with zero diagonals
        self.eps = eps
        self._initialize()

    def update_adjacency(self, new_adj):
        self.adj = new_adj
        self._initialize()

    def _initialize(self):
        n = self.adj.shape[0]
        self.means = np.zeros(n)
        adj_masked = self.adj.copy()
        np.fill_diagonal(adj_masked, 0)
        self.adj_masked = adj_masked
        self.converged = False

    def _inference(self):
        if self._verbosity > 0:
            print("running MF", flush = True)
        # >>>> YOUR CODE HERE >>>>>>>

        n = self.adj.shape[0]
        adj = self.adj
        t = 0
        temp = np.zeros(n)
        old_temp = np.ones(n)
        q = np.zeros(n)
        mean = 0

        while t < self.max_iters and np.max(np.abs(temp - old_temp)) > self.eps:
            t += 1

            old_temp = temp.copy()
            for i in range(n):
                temp[i] = adj[i,i]
                for j in range(n):
                    if self.adj[i,j] != 0 and j != i:
                        temp[i] += adj[i,j] * old_temp[j]
                temp[i] = 2*logistic(2*temp[i]) - 1
            temp = self.damping * temp + (1 - self.damping) * old_temp

            if self._verbosity > 0 and t % 100 == 0: print(t, np.mean(np.abs(temp - old_temp)))
        self.means = temp
        # >>>> YOUR CODE HERE >>>>>>>
        if self._verbosity > 0:
            print("finished after {} iterations!".format(t), flush = True)

    def get_marginal(self, target):
        if not self.converged:
            self._inference()
            self.converged = True
        # converting the mean ([-1,+1] range) to the p(x_i = 1) [0,1] range
        mrg = (self.means[target] + 1.)/2.
        return mrg
```

```python
#build a tree
tree = nx.grid_2d_graph(15,15)
n = nx.number_of_nodes(tree)
mask = np.array(nx.adjacency_matrix(tree).todense())
# create a random interaction over the edges
np.fill_diagonal(mask, 1.)
adj = mask * np.random.randn(n , n)
# make it symmetric
zero_inds = np.tril_indices_from(adj,-1)
adj[zero_inds] = 0
adj += adj.T.copy()
# do inference
true_margs = CliqueTreeInference(adj).get_marginal(list(range(adj.shape[0])))
mf_margs = MFInference(adj, verbosity=1).get_marginal(list(range(adj.shape[0])))

print(np.mean(np.abs(true_margs - mf_margs)))
assert np.mean(np.abs(true_margs - mf_margs)) < 1e-1, "MF marginals are not too different"
```


```python
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns
sns.set_context('notebook', font_scale=1.5)
```

The first exercise is about using Newton's method to find the cube roots of unity - find $z$ such that $z^3 = 1$. From the fundamental theorem of algebra, we know there must be exactly 3 complex roots since this is a degree 3 polynomial.

We start with Euler's equation
$$
e^{ix} = \cos x + i \sin x
$$

Raising $e^{ix}$ to the $n$th power where $n$ is an integer, we get from Euler's formula with $nx$ substituting for $x$
$$
(e^{ix})^n = e^{i(nx)} = \cos nx + i \sin nx
$$

Whenever $nx$ is an integer multiple of $2\pi$, we have
$$
\cos nx + i \sin nx = 1
$$

So
$$
e^{2\pi i \frac{k}{n}}
$$
is a root of 1 whenever $k/n = 0, 1, 2, \ldots$.

So the cube roots of unity are $1, e^{2\pi i/3}, e^{4\pi i/3}$.

While we can do this analytically, the idea is to use Newton's method to find these roots, and in the process, discover some rather perplexing behavior of Newton's method.

```python
from sympy import Symbol, exp, I, pi, N, expand
from sympy import init_printing
init_printing()
```

```python
expand(exp(2*pi*I/3), complex=True)
```

```python
expand(exp(4*pi*I/3), complex=True)
```

```python
plt.figure(figsize=(4,4))
roots = np.array([[1,0], [-0.5, np.sqrt(3)/2], [-0.5, -np.sqrt(3)/2]])
plt.scatter(roots[:,0], roots[:,1], s=50, c='red')
xp = np.linspace(0, 2*np.pi, 100)
plt.plot(np.cos(xp), np.sin(xp), c='blue');
```

**1**.  Newton's method for functions of complex variables - stability and basins of attraction. (30 points)

1. Write a function with the following function signature `newton(z, f, fprime, max_iter=100, tol=1e-6)` where
    - `z` is a starting value (a complex number e.g.  ` 3 + 4j`)
    - `f` is a function of `z`
    - `fprime` is the derivative of  `f`
The function will run until either max_iter is reached or the absolute value of the Newton step is less than tol. In either case, the function should return the number of iterations taken and the final value of `z` as a tuple (`i`, `z`).

2. Define the function `f` and `fprime` that will result in Newton's method finding the cube roots of 1. Find 3 starting points that will give different roots, and print both the start and end points.

Write the following two plotting functions to see some (pretty) aspects of Newton's algorithm in the complex plane.

3. The first function `plot_newton_iters(f, fprime, n=200, extent=[-1,1,-1,1], cmap='hsv')` calculates and stores the number of iterations taken for convergence (or max_iter) for each point in a 2D array. The 2D array limits are given by `extent` - for example, when `extent = [-1,1,-1,1]` the corners of the plot are `(-i, -i), (1, -i), (1, i), (-1, i)`. There are `n` grid points in both the real and imaginary axes. The argument `cmap` specifies the color map to use - the suggested defaults are fine. Finally plot the image using `plt.imshow` - make sure the axis ticks are correctly scaled. Make a plot for the cube roots of 1.

4. The second function `plot_newton_basins(f, fprime, n=200, extent=[-1,1,-1,1], cmap='jet')` has the same arguments, but this time the grid stores the identity of the root that the starting point converged to. Make a plot for the cube roots of 1 - since there are 3 roots, there should be only 3 colors in the plot.

```python
def newton(z, f, fprime, max_iter=100, tol=1e-6):
    """The Newton-Raphson method."""
    for i in range(max_iter):
        step = f(z)/fprime(z)
        if abs(step) < tol:
            return i, z
        z -= step
    return i, z
```

```python
def plot_newton_iters(p, pprime, n=200, extent=[-1,1,-1,1], cmap='hsv'):
    """Shows how long it takes to converge to a root using the Newton-Rahphson method."""
    m = np.zeros((n,n))
    xmin, xmax, ymin, ymax = extent
    for r, x in enumerate(np.linspace(xmin, xmax, n)):
        for s, y in enumerate(np.linspace(ymin, ymax, n)):
            z = x + y*1j
            m[s, r] = newton(z, p, pprime)[0]
    plt.imshow(m, cmap=cmap, extent=extent)
```

```python
def plot_newton_basins(p, pprime, n=200, extent=[-1,1,-1,1], cmap='jet'):
    """Shows basin of attraction for convergence to each root using the Newton-Raphson method."""
    root_count = 0
    roots = {}

    m = np.zeros((n,n))
    xmin, xmax, ymin, ymax = extent
    for r, x in enumerate(np.linspace(xmin, xmax, n)):
        for s, y in enumerate(np.linspace(ymin, ymax, n)):
            z = x + y*1j
            root = np.round(newton(z, p, pprime)[1], 1)
            if not root in roots:
                roots[root] = root_count
                root_count += 1
            m[s, r] = roots[root]
    plt.imshow(m, cmap=cmap, extent=extent)
```

```python
plt.grid('off')
plot_newton_iters(lambda x: x**3 - 1, lambda x: 3*x**2)
```

```python
plt.grid('off')
m = plot_newton_basins(lambda x: x**3 - 1, lambda x: 3*x**2)
```

**2**.  Ill-conditioned linear problems. (20 points)

You are given a $n \times p$ design matrix $X$ and a $p$-vector of observations $y$ and asked to find the coefficients $\beta$ that solve the linear equations $X \beta = y$.
```python
X = np.load('x.npy')
y = np.load('y.npy')
```

The solution $\beta$ can also be loaded as
```python
beta = np.load('b.npy')
```

- Write a formula that could solve the system of linear equations in terms of $X$ and $y$ Write a function `f1` that takes arguments $X$ and $y$ and returns $\beta$ using this formula.
- How could you code this formula using `np.linalg.solve` that does not require inverting a matrix? Write a function `f2` that takes arguments $X$ and $y$ and returns $\beta$ using this.
- Note that carefully designed algorithms *can* solve this ill-conditioned problem, which is why you should always use library functions for linear algebra rather than write your own.
```python
np.linalg.lstsq(x, y)[0]
```
- What happens if you try to solve for $\beta$ using `f1` or `f2`? Remove the column of $X$ that is making the matrix singular and find the $p-1$ vector $b$ using `f2`.
- Note that the solution differs from that given by `np.linalg.lstsq`? This arises because the relevant condition number for `f2` is actually for the matrix $X^TX$ while the condition number of `lstsq` is for the matrix $X$. Why is the condition so high even after removing the column that makes the matrix singular?

```python
X = np.load('x.npy')
y = np.load('y.npy')
beta = np.load('b.npy')

def f1(X, y):
    """Direct translation of normal equations to code."""
    return np.dot(np.linalg.inv(np.dot(X.T, X)), np.dot(X.T, y))

def f2(X, y):
    """Solving normal equations wihtout matrix inversion."""
    return np.linalg.solve(np.dot(X.T, X), np.dot(X.T, y))

%precision 3
print("X = ")
print(X)
# counting from 0 (so column 5 is the last column)
# we can see that column 5 is a multiple of column 3
# so one approach is to simply remove this (dependent) column

print("True solution\t\t", beta)
print("Library function\t", np.linalg.lstsq(X, y)[0])
print("Using f1\t\t", f1(X[:, :5], y))
print("Using f2\t\t", f2(X[:, :5], y))
```

    X =
    [[  5.000e+00   4.816e+14   9.000e+00   5.000e+00   0.000e+00   5.000e+01]
     [  1.000e+00   4.214e+14   6.000e+00   9.000e+00   2.000e+00   9.000e+01]
     [  5.000e+00   1.204e+14   4.000e+00   2.000e+00   4.000e+00   2.000e+01]
     [  7.000e+00   5.418e+14   1.000e+00   7.000e+00   0.000e+00   7.000e+01]
     [  9.000e+00   5.418e+14   7.000e+00   6.000e+00   9.000e+00   6.000e+01]
     [  0.000e+00   6.020e+13   8.000e+00   8.000e+00   3.000e+00   8.000e+01]
     [  8.000e+00   4.214e+14   3.000e+00   6.000e+00   5.000e+00   6.000e+01]
     [  9.000e+00   1.806e+14   4.000e+00   8.000e+00   1.000e+00   8.000e+01]
     [  0.000e+00   1.806e+14   9.000e+00   2.000e+00   0.000e+00   2.000e+01]
     [  9.000e+00   1.204e+14   7.000e+00   7.000e+00   9.000e+00   7.000e+01]]
    True solution		 [ 0.469  0.096  0.903  0.119  0.525  0.084]
    Library function	 [ 0.469  0.096  0.903  0.009  0.526  0.095]
    Using f1		 [ 0.465  0.096  0.908  0.951  0.523]
    Using f2		 [ 0.471  0.096  0.903  0.953  0.526]

#### Condition numbers are ratio of largest to smallest singular values

```python
np.linalg.svd(X)[1]
```

    array([  1.128e+15,   1.124e+02,   1.364e+01,   1.151e+01,   6.344e+00,
             9.940e-16])

```python
np.linalg.svd(X[:, :-1])[1]
```

    array([  1.128e+15,   1.863e+01,   1.224e+01,   8.025e+00,   6.086e+00])

```python
np.linalg.cond(X[:, :-1])
```

One way to think about the condition number is in terms of the ratio of the largest singular value to the smallest one - so a measure of the disproportionate stretching effect of the linear transform in one direction versus another. When this is very big, it means that errors in one or more direction will be amplified greatly.  This often occurs because one or more columns is "almost" dependent - i.e.  it can be approximated by a linear combination of the other columns.

**3**. Consider the following function on $\mathbb{R}^2$:

$$f(x_1,x_2) = -x_1x_2e^{-\frac{(x_1^2+x_2^2)}{2}}$$

1. Write down its gradient.
2. write down the Hessian matrix.
3. Find the critical points of $f$.
4. Characterize the critical points as max/min or neither. Find the minimum under the constraint
$$g(x) = x_1^2+x_2^2 \leq 10$$
and
$$h(x) = 2x_1 + 3x_2 = 5$$ using `scipy.optimize.minimize`.
5. Plot the function contours using `matplotlib`. (20 points)

```python
import sympy as sym
from sympy import Matrix
from numpy import linalg as la

x1, x2 = sym.symbols('x1 x2')
def f(x1, x2):
    return sym.Matrix([-x1*x2*sym.exp(-(x1**2 + x2**2)/2)])

def h(x1,x2):
    return x1**2+x2**2

def g(x1,x2):
    return 2*x1+3*x2

def characterize_cp(H):
    l,v = la.eig(H)
    if(np.all(np.greater(l,np.zeros(2)))):
       return("minimum")
    elif(np.all(np.less(l,np.zeros(2)))):
       return("maximum")
    else:
       return("saddle")
```

```python
sym.init_printing()
```

```python
fun = f(x1,x2)
X = sym.Matrix([x1,x2])

gradf = fun.jacobian(X)
sym.simplify(gradf)
```

```python
hessianf = gradf.jacobian(X)
sym.simplify(hessianf)
```

```python
fcritical = sym.solve(gradf,X)

for i in range(4):
    H = np.array(hessianf.subs([(x1,fcritical[i][0]),(x2,fcritical[i][1])])).astype(float)
    print(fcritical[i], characterize_cp(H))
```

    (-1, -1) minimum
    (-1, 1) maximum
    (0, 0) saddle
    (1, -1) maximum

```python
import scipy.optimize as opt
```

```python
def f(x):
    return -x[0] * x[1] * np.exp(-(x[0]**2+x[1]**2)/2)

cons = ({'type': 'eq',
         'fun' : lambda x: np.array([2.0*x[0] + 3.0*x[1] - 5.0]),
         'jac' : lambda x: np.array([2.0,3.0])},
        {'type': 'ineq',
         'fun' : lambda x: np.array([-x[0]**2.0 - x[1]**2.0 + 10.0])})

x0 = [1.5,1.5]
cx = opt.minimize(f, x0, constraints=cons)
```

```python
x = np.linspace(-5, 5, 200)
y = np.linspace(-5, 5, 200)
X, Y = np.meshgrid(x, y)
Z = f(np.vstack([X.ravel(), Y.ravel()])).reshape((200,200))

plt.contour(X, Y, Z)

plt.plot(x, (5-2*x)/3, 'k:', linewidth=1)
plt.plot(x, (10.0-x**2)**0.5, 'k:', linewidth=1)
plt.plot(x, -(10.0-x**2)**0.5, 'k:', linewidth=1)
plt.fill_between(x,(10-x**2)**0.5,-(10-x**2)**0.5,alpha=0.15)
plt.text(cx['x'][0], cx['x'][1], 'x', va='center', ha='center', size=20, color='red')
plt.axis([-5,5,-5,5])
plt.title('Contour plot of f(x) subject to constraints g(x) and h(x)')
plt.xlabel('x1')
plt.ylabel('x2')
pass
```

**4**. One of the goals of the course it that you will be able to implement novel algorithms from the literature.  (30 points)

- Implement the mean-shift algorithm in 1D as described [here](http://homepages.inf.ed.ac.uk/rbf/CVonline/LOCAL_COPIES/TUZEL1/MeanShift.pdf).
    - Use the following function signature
    ```python
    def mean_shift(xs, x, kernel, max_iters=100, tol=1e-6):
    ```
    - xs is the data set, x is the starting location, and kernel is a kernel function
    - tol is the difference in $||x||$ across iterations
- Use the following kernels with bandwidth $h$ (a default value of 1.0 will work fine)
    - Flat - return 1 if $||x|| < h$ and 0 otherwise
    - Gaussian
    $$\frac{1}{\sqrt{2 \pi h}}e^{\frac{-||x||^2}{h^2}}$$
    - Note that $||x||$ is the norm of the data point being evaluated minus the current value of $x$
- Use both kernels to find all 3 modes of the data set in `x1d.npy`
- Modify the algorithm and/or kernels so that it now works in an arbitrary number of dimensions.
- Use both kernels to find all 3 modes of the data set in `x2d.npy`
- Plot the path of successive intermediate solutions of the mean-shift algorithm starting from `x0 = (-4, 5)` until it converges onto a mode in the 2D data for each kernel. Superimpose the path on top of a contour plot of the data density.

```python
def gaussian_kernel(xs, x, h=1.0):
    """Gaussian kernel for a shifting window centerd at x."""

    X = xs - x
    try:
        d = xs.shape[1]
    except:
        d = 1
    k = np.array([(2*np.pi*h**d)**-0.5*np.exp(-(np.dot(_.T, _)/h)**2) for _ in X])
    if d != 1:
        k = k[:, np.newaxis]
    return k

def flat_kernel(xs, x, h=1.0):
    """Flat kenrel for a shifting window centerd at x."""

    X = xs - x
    try:
        d = xs.shape[1]
    except:
        d = 1
    k = np.array([1 if np.dot(_.T, _) < h else 0 for _ in X])
    if d != 1:
        k = k[:, np.newaxis]
    return k

def mean_shift(xs, x, kernel, max_iters=100, tol=1e-6, trace=False):
    """Finds the local mode using mean shift algorithm."""

    record = []

    for i in range(max_iters):
        if trace:
            record.append(x)
        m = (kernel(xs, x)*xs).sum(axis=0)/kernel(xs, x).sum(axis=0) - x
        if np.sum(m**2) < tol:
            break
        x += m
    return i, x, np.array(record)
```

```python
x1 = np.load('x1d.npy')

# choose kernel to evaluate
kernel = flat_kernel
# kernel = gaussian_kernel

i1, m1, path = mean_shift(x1, 1, kernel)
print(i1, m1)

i2, m2, path = mean_shift(x1, -7, kernel)
print(i2, m2)

i3, m3, path = mean_shift(x1, 7 ,kernel)
print(i3, m3)

xp = np.linspace(0, 1.0, 100)
plt.hist(x1, 50, histtype='step', normed=True);
plt.axvline(m1, c='blue')
plt.axvline(m2, c='blue')
plt.axvline(m3, c='blue');
```

```python
x2 = np.load('x2d.npy')

# choose kernel to evaluate
# kernel = flat_kernel (also OK if they use the Epanachnikov kernel since the flat is a shadow of that)
kernel = gaussian_kernel

i1, m1, path1 = mean_shift(x2, [0,0], kernel, trace=True)
print(i1, m1)

i2, m2, path2 = mean_shift(x2, [-4,5], kernel, trace=True)
print(i2, m2)

i3, m3, path3 = mean_shift(x2, [10,10] ,kernel, trace=True)
print(i3, m3)
```

    59 [ 2.318  2.826]
    12 [-3.07   3.057]
    42 [ 6.023  8.951]

```python
import scipy.stats as stats

# size of marekr at starting position
base = 40

# set up for estimating density using gaussian_kde
xmin, xmax = -6, 12
ymin,ymax = -5, 15
X, Y = np.mgrid[xmin:xmax:100j, ymin:ymax:100j]
positions = np.vstack([X.ravel(), Y.ravel()])
kde = stats.gaussian_kde(x2.T)
Z = np.reshape(kde(positions).T, X.shape)

plt.contour(X, Y, Z)
# plot data in background
plt.scatter(x2[:, 0], x2[:, 1], c='grey', alpha=0.2, edgecolors='none')

# path from [0,0]
plt.scatter(path1[:, 0], path1[:, 1], s=np.arange(base, base+len(path1)),
            c='red', edgecolors='red', marker='x', linewidth=1.5)

# path from [-4,5]
plt.scatter(path2[:, 0], path2[:, 1], s=np.arange(base, base+len(path2)),
            c='blue', edgecolors='blue', marker='x', linewidth=1.5)

# path from [10,10]
plt.scatter(path3[:, 0], path3[:, 1], s=np.arange(base, base+len(path3)),
            c='green', edgecolors='green',marker='x', linewidth=1.5)
plt.axis([xmin, xmax, ymin, ymax]);
```


```python
%matplotlib inline
from IPython.display import display,Math
from sympy import *
init_session()
```

```python
from IPython.display import HTML
from ipywidgets import interact
from ipywidgets import interact,Dropdown,IntSlider
@interact
def _(p="202112",n="3456"):
    p = p[:6]
    pstr = "{:06d}".format(int(p))
    p = int(pstr)
    n = n[:4]
    nstr = "{:04d}".format(int(n))
    n = int(nstr)
    m = pstr + nstr
    clist = ["A","B","C","H","K","M","R","U","X","Y","Z"]
    sum = 0
    for i in range(len(m)):
        sum += int(m[i])*(i+1)
    c = sum%11
    m = m + clist[c]
    print("会場：{}\n番号：{}\nのチェックディジットは{}なので\n受験番号：{}\nとなります".format(p,n,c,m))
```

以下は[ここ](https://ja.wikipedia.org/wiki/Luhn%E3%82%A2%E3%83%AB%E3%82%B4%E3%83%AA%E3%82%BA%E3%83%A0)
にあったコードを用いて作成した．

```python
def check_number(digits):
    _sum = 0
    alt = False
    for d in reversed(str(digits)):
        d = int(d)
        assert 0 <= d <= 9
        if alt:
            d *= 2
            if d > 9:
                d -= 9
        _sum += d
        alt = not alt
    return (_sum % 10) == 0

from IPython.display import HTML
from ipywidgets import interact
from ipywidgets import interact,Dropdown,IntSlider
@interact
def _(n="49927398716"):
    check = check_number(n)
    if check:
        print("{}は正しい".format(n))
    else:
        print("{}は正しくない".format(n))
```

```python

```


# Digital agro

# Sensitivity analysis of complex models

# Seminar - 24.04.2020

### MONICA model

### Main

Consider the model output as
\begin{eqnarray*}
Y=f(X)=f\left(X_{1}, \ldots, X_{p}\right),
\end{eqnarray*}
where $f$ in our case part of agro-model simulator, $X$ are $p$ varied input parameters and $Y$ is the predicted output. Following the techniques by Sobol we represent the multi-variate random function $f$ using Hoeffding decomposition:
\begin{equation}\label{eq:hoeffding}
f(X_1,\dots,X_p) = f_0 + \sum_i^p f_i + \sum_i^p\sum_{j>i}^p f_{ij} + \dots + f_{1\dots p},
\end{equation}
where $f_0$ is a constant term, $f_i = f_i(X_i)$ denotes main effects, $f_{ij} = f_{ij}(X_i, X_j)$ and others describe higher-order interactions. These terms can be written as
\begin{equation*}
\begin{split}
f_0 &= E(Y),\\
f_i &= E_{X_{\sim i}}(Y | X_i) - E(Y),\\
f_{ij} &= E_{X_{\sim ij}}(Y | X_i, X_j) - f_i - f_j - f_0,\\
\dots
\end{split}
\end{equation*}
where $E$ is mathematical expectation and $X_{\sim i}$ denotes all parameters except $i^\text{th}$. Under the assumption that the input parameters are independent, total variance $V(Y)$ of the crop yield can be decomposed as follows:
\begin{equation*}
V(Y) = \sum_i^p V_i + \sum_i^p\sum_{j>i}^p V_{ij} + \dots + V_{12\dots p},
\end{equation*}
where partial variances are
\begin{equation*}
\begin{split}
V_i &= V[f_i(X_i)] = V_{X_i}\left[E_{X_{\sim i}}(Y | X_i)\right],\\
V_{ij} &= V[f_{ij}(X_i,X_j)] = V_{X_iX_j}\left[E_{X_{\sim ij}}(Y | X_i, X_j)\right] - V_i - V_j,\\
\dots
\end{split}
\end{equation*}

## Sobol index (first order, second order, total index)

This way, sensitivity indices (SI) can be introduced as
\begin{equation}
S_i = \frac{V_i}{V(Y)},~S_{ij} = \frac{V_{ij}}{V(Y)},~\dots
\end{equation}
In order to incorporate all of the interactions for a particular parameter, one can compute the total effect index:
\begin{equation}
S_{T_i} = \frac{E_{X_{\sim i}}\left[V_{X_i}(Y|X_{\sim i})\right]}{V(Y)} = 1 - \frac{V_{X_{\sim i}}\left[E_{X_i}(Y | X_{\sim i})\right]}{V(Y)}
\end{equation}

-------------

# Problem - input samples for model: how to generate?

#### Sobol’ Sequences versus Random numbers and regular grid

More -
* [Wiki](https://en.wikipedia.org/wiki/Sobol_sequence)
* [Habr](https://habr.com/ru/post/440892/)

### Modern Quasi-Random Sequences

# Running PCSE/WOFOST with custom input data (1)

This Jupyter notebook will show you how to read inputs from files for running PCSE/WOFOST.

thanks to **Allard de Wit, March 2018**

**Prerequisites for running this notebook**

Several packages need to be installed for running PCSE/WOFOST:

  1. `PCSE` and its dependencies. See the [PCSE user guide](http://pcse.readthedocs.io/en/stable/installing.html) for more information;
  2. The `pandas` module for processing and storing WOFOST output;
  3. The `matplotlib` module for generating charts

## Introduction

For running PCSE/WOFOST (and PCSE models in general) with your own data sources you need three different types of inputs:

 1. Model parameters that parameterize the different model components. These parameters usually consist of a set of crop parameters (or multiple sets in case of crop rotations), a set of soil parameters and a set of site parameters. The latter provide ancillary parameters that are specific for a location, for example the initial amount of moisture in the soil.
 2. Driving variables represented by weather data which can be derived from various sources.
 3. Agromanagement actions which specify the farm activities that will take place on the field that is simulated by PCSE.

For this example we will run a simulation for sugar beet in Wageningen (Netherlands) and we will read the input data step by step from several different sources instead of using the pre-configured start_wofost() script. For the example we will assume that data files are in the `data` directory within the directory where this notebook is located. This will be the case if you downloaded the notebooks from github.

## Importing the relevant modules

```python
%matplotlib inline
import sys, os
import matplotlib
matplotlib.style.use("ggplot")
import matplotlib.pyplot as plt
import pandas as pd
import yaml
import numpy as np

data_dir = os.path.join(os.getcwd(), "data")
import pcse
print("This notebook was built with:")
print("python version: %s " % sys.version)
print("PCSE version: %s" %  pcse.__version__)
```

    Platform not recognized, using system temp directory for PCSE settings.
    Platform not recognized, using system temp directory for PCSE settings.

    Building PCSE demo database at: /var/folders/18/7s50f67955z9qwzwf3d3v8cm0000gn/T/.pcse/pcse.db ... OK
    This notebook was built with:
    python version: 3.7.6 (default, Jan  8 2020, 13:42:34)
    [Clang 4.0.1 (tags/RELEASE_401/final)]
    PCSE version: 5.4.2

## Reading model parameters
### Crop parameters

The crop parameters consist of parameter names and the corresponding parameter values that are needed to parameterize the components of the crop simulation model. These are crop-specific values regarding phenology, assimilation, respiration, biomass partitioning, etc. The parameter file for sugar beet is taken from the crop files in the WOFOST Control Centre.

As many crop models in Wageningen were written in FORTRAN, the crop parameters for many models in Wageningen are often provided in the CABO format that could be read with the TTUTIL FORTRAN library. This CABO format will be gradually phased out and PCSE will move to a new format based on YAML, see [here for an example](https://github.com/ajwdewit/WOFOST_crop_parameters/blob/master/wheat.yaml). However, PCSE tries to be backward compatible as much as possible and provides the `CABOFileReader` for reading parameter files in CABO format. The `CABOFileReader` returns a dictionary with the parameter name/value pairs:

```python
from pcse.fileinput import CABOFileReader
cropfile = os.path.join(data_dir, 'crop', 'sug0601.crop')
cropdata = CABOFileReader(cropfile)
```

```python
# Number of parameters for our crop
len(cropdata)
```

    63

```python
#cropdata
```

### Soil parameters
The soildata dictionary provides the parameter name/value pairs related to the soil type and soil physical properties. The number of parameters is variable depending on the soil water balance type that is used for the simulation. For this example, we will use the water balance for freely draining soils and use the soil file for medium fine sand: `ec3.soil`. This file is also taken from the soil files in the [WOFOST Control Centre](http://www.wageningenur.nl/wofost).

```python
soilfile = os.path.join(data_dir, 'soil', 'ec3.soil')
soildata = CABOFileReader(soilfile)
```

```python
len(soildata)
```

    16

```python
#soildata
```

### Site parameters

The site parameters provide ancillary parameters that are not related to the crop or the soil. Examples are the initial conditions of the water balance such as the initial soil moisture content (WAV) and the initial and maximum surface storage (SSI, SSMAX). Also the atmospheric $CO_{2}$
concentration is a typical site parameter. For the moment, we can define these parameters directly on the Python commandline as a simple python dictionary. However, it is more convenient to use the `WOFOST71SiteDataProvider` that documents the site parameters and provides sensible defaults:

```python
from pcse.util import WOFOST71SiteDataProvider
sitedata = WOFOST71SiteDataProvider(WAV=100, CO2=360)
print(sitedata)
```

    {'IFUNRN': 0, 'NOTINF': 0, 'SSI': 0.0, 'SSMAX': 0.0, 'WAV': 100.0, 'SMLIM': 0.4, 'CO2': 360.0}

### Packaging all parameters
Finally, we need to pack the different sets of parameters into one variable using the `ParameterProvider`. This is needed because PCSE expects one variable that contains all parameter values. Using this approach has the additional advantage that parameter value can be easily overridden in case of running multiple simulations with slightly different parameter values:

```python
from pcse.base import ParameterProvider
parameters = ParameterProvider(cropdata=cropdata, soildata=soildata, sitedata=sitedata)
```

## Agromanagement
The agromanagement inputs provide the start date of the agricultural campaign, the start_date/start_type of the crop simulation, the end_date/end_type of the crop simulation and the maximum duration of the crop simulation. The latter is included to avoid unrealistically long simulations for example as a results of a too high temperature sum requirement.

The agromanagement inputs are defined with a special syntax called [YAML](http://yaml.org/) which allows to easily create more complex structures which is needed for defining the agromanagement. The agromanagement file for sugar beet in Wageningen `sugarbeet_calendar.agro` can be read with the `YAMLAgroManagementReader`:

```python
from pcse.fileinput import YAMLAgroManagementReader
agromanagement_file = os.path.join(data_dir, 'agro', 'sugarbeet_calendar.agro')
agromanagement = YAMLAgroManagementReader(agromanagement_file)
#print(agromanagement)
```

    /Users/iMac/miniconda3/lib/python3.7/site-packages/pcse/fileinput/yaml_agro_loader.py:21: YAMLLoadWarning: calling yaml.load() without Loader=... is deprecated, as the default Loader is unsafe. Please read https://msg.pyyaml.org/load for full details.
      r = yaml.load(fp)

```python
K_kg = 60
P_kg = 60
N_kg = 120
year_date=2019
yaml_agro = f"""
- {year_date}-06-01:
    CropCalendar:
        crop_name: 'sugar-beet'
        variety_name: 'sugar-beet-601'
        crop_start_date: {year_date}-06-02
        crop_start_type: emergence
        crop_end_date: {year_date}-10-15
        crop_end_type: harvest
        max_duration: 300
    TimedEvents:
    -   event_signal: apply_npk
        name:  Timed N/P/K application table
        comment: All fertilizer amounts in kg/ha
        events_table:
        - {year_date}-06-22: {{N_amount : {N_kg}, P_amount: {P_kg}, K_amount: {K_kg}}}
    StateEvents: null
"""
#agromanagement = yaml.safe_load(yaml_agro)
```

## Daily weather observations
Daily weather variables are needed for running the simulation. There are several data providers in PCSE for reading weather data, see the section on [weather data providers](http://pcse.readthedocs.io/en/stable/reference_guide.html#weather-data-providers) to get an overview.

For this example we will use weather data from an excel file which provides daily weather data for Wageningen for the period 2004 to 2008. We will read the data from the file using the ExcelWeatherDataProvider:

### NASA Weather Data Provider

```python
#NASA Weather system
from pcse.db import NASAPowerWeatherDataProvider
```

```python
moscow_weather = NASAPowerWeatherDataProvider(55.45, 37.36)
print(moscow_weather)
```

    Weather data provided by: NASAPowerWeatherDataProvider
    --------Description---------
    NASA/POWER SRB/FLASHFlux/MERRA2/GEOS 5.12.4 (FP-IT) 0.5 x 0.5 Degree Daily Averaged Data
    ----Site characteristics----
    Elevation:  181.1
    Latitude:  55.450
    Longitude: 37.360
    Data available for 1983-07-01 - 2020-04-16
    Number of missing days: 8

__We have number of missed values__

__Lets fill them__

```python
df_weather = pd.DataFrame(moscow_weather.export())
```

```python
r = pd.date_range(start=df_weather.DAY.min(), end=df_weather.DAY.max())
full_range_weather = df_weather.set_index('DAY').reindex(r).\
                rename_axis('DAY').reset_index()
```

```python
full_range_weather[full_range_weather.isna().any(axis=1)]
```

      DAY
      LAT
      LON
      ELEV
      IRRAD
      TMIN
      TMAX
      VAP
      RAIN
      E0
      ES0
      ET0
      WIND
      TEMP

      8999
      2008-02-19
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN

      9038
      2008-03-29
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN

      9758
      2010-03-19
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN

      11075
      2013-10-26
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN

      11676
      2015-06-19
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN

      11897
      2016-01-26
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN

      12641
      2018-02-08
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN

      12921
      2018-11-15
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN

__Fill__

```python
filled_weather = full_range_weather.fillna(method='ffill', axis=0)
```

```python
filled_weather.query('DAY=="2008-02-19"|DAY=="2008-02-18"')
```

      DAY
      LAT
      LON
      ELEV
      IRRAD
      TMIN
      TMAX
      VAP
      RAIN
      E0
      ES0
      ET0
      WIND
      TEMP

      8998
      2008-02-18
      55.45
      37.36
      181.05
      4130000.0
      -10.46
      -1.38
      3.633086
      0.262
      0.012311
      0.00701
      0.034524
      4.3
      -6.09

      8999
      2008-02-19
      55.45
      37.36
      181.05
      4130000.0
      -10.46
      -1.38
      3.633086
      0.262
      0.012311
      0.00701
      0.034524
      4.3
      -6.09

# SAVE AS CSV

```python
from pcse.fileinput import CSVWeatherDataProvider
```

```python
CSVWeatherDataProvider('./weather_full.csv')
```

    <pcse.fileinput.csvweatherdataprovider.CSVWeatherDataProvider at 0x122cdf750>

```python
# header_moscow = """
# ## Site Characteristics
# Country     = 'Russia'
# Station     = 'Moscow'
# Description = 'Observed data from NASA'
# Source      = 'NASA'
# Contact     = 'Misha Grol'
# Longitude = 5.67; Latitude = 51.97; Elevation = 7; AngstromA = 0.18; AngstromB = 0.55; HasSunshine = False
# ## Daily weather observations (missing values are NaN)
# """
```

```python
# filled_weather.to_csv('moscow_full_weather.csv', sep=',', index=False)
```

Weather loader for __NH__ example

```python
from pcse.fileinput import ExcelWeatherDataProvider
weatherfile = os.path.join(data_dir, 'meteo', 'nl1.xlsx')
wdp = ExcelWeatherDataProvider(weatherfile)
print(wdp)
```

    Weather data provided by: ExcelWeatherDataProvider
    --------Description---------
    Weather data for:
    Country: Netherlands
    Station: Wageningen, Location Haarweg
    Description: Observed data from Station Haarweg in Wageningen
    Source: Meteorology and Air Quality Group, Wageningen University
    Contact: Peter Uithol
    ----Site characteristics----
    Elevation:    7.0
    Latitude:  51.970
    Longitude:  5.670
    Data available for 2004-01-02 - 2008-12-31
    Number of missing days: 0

## Importing, initializing and running a PCSE model

Internally, PCSE uses a simulation engine to run a crop simulation. This engine takes a configuration file that specifies the components for the crop, the soil and the agromanagement that need to be used for the simulation. So any PCSE model can be started by importing the engine and initializing it with a given configuration file and the corresponding parameters, weather data and agromanagement.

However, as many users of PCSE only need a particular configuration (for example the WOFOST model for potential production), preconfigured Engines are provided in `pcse.models`. For the sugarbeet example we will import the WOFOST model for water-limited simulation under freely draining soil conditions:

```python
from pcse.models import Wofost71_WLP_FD
#wofsim = Wofost71_WLP_FD(parameters, moscow_weather, agromanagement)
```

```python
%%time
from pcse.models import Wofost71_WLP_FD
wofsim = Wofost71_WLP_FD(parameters, wdp, agromanagement)
wofsim.run_till_terminate()
df_results= pd.DataFrame(wofsim.get_output())
df_results = df_results.set_index("day")
df_results.tail()
```

    CPU times: user 576 ms, sys: 3.75 ms, total: 579 ms
    Wall time: 696 ms

      DVS
      LAI
      TAGP
      TWSO
      TWLV
      TWST
      TWRT
      TRA
      RD
      SM
      WWLOW

      day

      2006-10-16
      2.0
      1.048787
      21557.705173
      14410.185356
      3408.824045
      3738.695772
      2805.686837
      0.052878
      120.0
      0.283340
      34.000845

      2006-10-17
      2.0
      1.055901
      21628.846417
      14474.212476
      3412.381108
      3742.252834
      2807.138699
      0.030516
      120.0
      0.282876
      33.945161

      2006-10-18
      2.0
      1.049414
      21658.771700
      14501.145230
      3413.877372
      3743.749098
      2807.749419
      0.030184
      120.0
      0.284605
      34.152545

      2006-10-19
      2.0
      1.052133
      21685.964928
      14525.619135
      3415.237033
      3745.108759
      2808.304383
      0.043053
      120.0
      0.284840
      34.180768

      2006-10-20
      2.0
      1.043584
      21717.141971
      14553.678474
      3416.795885
      3746.667611
      2808.940650
      0.039061
      120.0
      0.284271
      34.112520

We can then run the simulation and retrieve the time series of daily simulation output using the get_output() method on the WOFOST object. Finally, we convert the simulation results to a pandas dataframe:

```python
summary_output = wofsim.get_summary_output()
```

```python
wofsim.get_summary_output()
```

    [{'DVS': 2.0,
      'LAIMAX': 5.367243094787344,
      'TAGP': 21717.14197118098,
      'TWSO': 14553.678474455048,
      'TWLV': 3416.7958853006894,
      'TWST': 3746.6676114252436,
      'TWRT': 2808.940649509447,
      'CTRAT': 29.87844231452199,
      'RD': 120.0,
      'DOS': None,
      'DOE': datetime.date(2006, 4, 5),
      'DOA': datetime.date(2006, 6, 15),
      'DOM': datetime.date(2006, 9, 17),
      'DOH': datetime.date(2006, 10, 20),
      'DOV': None}]

```python
msg = "Reached maturity at {DOM} with total biomass {TAGP} kg/ha "\
"and a yield of {TWSO} kg/ha."
print(msg.format(**summary_output[0]))
```

    Reached maturity at 2006-09-17 with total biomass 21717.14197118098 kg/ha and a yield of 14553.678474455048 kg/ha.

# SA PCSE

```python
NPK = {
    'num_vars':3,
    'names':['N', 'P', 'K'],
    'bounds':[[30., 60.],
              [60., 90.],
              [100., 130.]]
}
```

```python
Soil_parameters = {
  'num_vars':5,
    'names':['SMV', 'SMFCF', 'SM0', 'CRAIRC', 'K0'],
    'bounds':[[0.7, 1.3],
              [0.1, 0.5],
              [0.2, 0.6],
             [0.04, 0.08],
             [22.5, 27.5]]}
```

```python
param_values = saltelli.sample(Soil_parameters, 10)
```

$n = N \times (D \times 2 +2)$

```python
param_values.shape
```

    (120, 5)

```python
K_kg = 60
P_kg = 60
N_kg = 120
year_date=2019
yaml_agro = f"""
- {year_date}-06-01:
    CropCalendar:
        crop_name: 'sugar-beet'
        variety_name: 'sugar-beet-601'
        crop_start_date: {year_date}-06-02
        crop_start_type: emergence
        crop_end_date: {year_date}-10-15
        crop_end_type: harvest
        max_duration: 300
    TimedEvents:
    -   event_signal: apply_npk
        name:  Timed N/P/K application table
        comment: All fertilizer amounts in kg/ha
        events_table:
        - {year_date}-06-22: {{N_amount : {N_kg}, P_amount: {P_kg}, K_amount: {K_kg}}}
    StateEvents: null
"""
agromanagement = yaml.load(yaml_agro)
```

## Input samples

```python
from pcse.fileinput import YAMLAgroManagementReader
agromanagement_file = os.path.join(data_dir, 'agro', './sugarbeet_calendar.agro')
agromanagement = YAMLAgroManagementReader(agromanagement_file)
#print(agromanagement)
Soil_parameters = {
  'num_vars':5,
    'names':['SMV', 'SMFCF', 'SM0', 'CRAIRC', 'K0'],
    'bounds':[[0.7, 1.3],
              [0.1, 0.5],
              [0.2, 0.6],
             [0.04, 0.08],
             [22.5, 27.5]]}
param_values = saltelli.sample(Soil_parameters, N=10, calc_second_order=True)
```

### Loop for yield prediction

```python
def sensitivity_soil(soil_parameters):
    SMV, SMFCF, SM0, CRAIRC, K0 = soil_parameters
    soildata['SMV'] = SMV
    soildata['SMFCF'] = SMFCF
    soildata['SM0'] = SM0
    soildata['CRAIRC'] = CRAIRC
    soildata['K0'] = K0
    parameters = ParameterProvider(cropdata=cropdata, soildata=soildata, sitedata=sitedata)
    wofsim = Wofost71_WLP_FD(parameters, wdp, agromanagement)
    wofsim.run_till_terminate()
    #df_results = pd.DataFrame(wofsim.get_output())
    #df_results = df_results.set_index("day")
    #df_results.tail()
    summary_output = wofsim.get_summary_output()
    yield_list.append(summary_output[0]['TWSO'])
```

```python
sensitivity_soil(param_values[30])
```

```python
param_values[30]
```

    array([ 1.28183594,  0.21835938,  0.25976562,  0.05003906, 26.59667969])

```python
%%time
yield_list = []
param_values = saltelli.sample(Soil_parameters, 10, calc_second_order=True)
for st in range(len(param_values)):
    sensitivity_soil(param_values[st])
    #print(param_values[st])
```

    [ 0.83183594  0.13867188  0.40742187  0.06707031 23.90136719]
    [ 1.24433594  0.13867188  0.40742187  0.06707031 23.90136719]
    [ 0.83183594  0.11835938  0.40742187  0.06707031 23.90136719]
    [ 0.83183594  0.13867188  0.55976563  0.06707031 23.90136719]
    [ 0.83183594  0.13867188  0.40742187  0.06003906 23.90136719]
    [ 0.83183594  0.13867188  0.40742187  0.06707031 22.84667969]
    [ 0.83183594  0.11835938  0.55976563  0.06003906 22.84667969]
    [ 1.24433594  0.13867188  0.55976563  0.06003906 22.84667969]
    [ 1.24433594  0.11835938  0.40742187  0.06003906 22.84667969]
    [ 1.24433594  0.11835938  0.55976563  0.06707031 22.84667969]
    [ 1.24433594  0.11835938  0.55976563  0.06003906 23.90136719]
    [ 1.24433594  0.11835938  0.55976563  0.06003906 22.84667969]
    [ 1.13183594  0.33867188  0.20742188  0.04707031 26.40136719]
    [ 0.94433594  0.33867188  0.20742188  0.04707031 26.40136719]
    [ 1.13183594  0.31835938  0.20742188  0.04707031 26.40136719]
    [ 1.13183594  0.33867188  0.35976562  0.04707031 26.40136719]
    [ 1.13183594  0.33867188  0.20742188  0.04003906 26.40136719]
    [ 1.13183594  0.33867188  0.20742188  0.04707031 25.34667969]
    [ 1.13183594  0.31835938  0.35976562  0.04003906 25.34667969]
    [ 0.94433594  0.33867188  0.35976562  0.04003906 25.34667969]
    [ 0.94433594  0.31835938  0.20742188  0.04003906 25.34667969]
    [ 0.94433594  0.31835938  0.35976562  0.04707031 25.34667969]
    [ 0.94433594  0.31835938  0.35976562  0.04003906 26.40136719]
    [ 0.94433594  0.31835938  0.35976562  0.04003906 25.34667969]
    [ 1.28183594  0.23867188  0.50742187  0.07707031 25.15136719]
    [ 0.79433594  0.23867188  0.50742187  0.07707031 25.15136719]
    [ 1.28183594  0.21835938  0.50742187  0.07707031 25.15136719]
    [ 1.28183594  0.23867188  0.25976562  0.07707031 25.15136719]
    [ 1.28183594  0.23867188  0.50742187  0.05003906 25.15136719]
    [ 1.28183594  0.23867188  0.50742187  0.07707031 26.59667969]
    [ 1.28183594  0.21835938  0.25976562  0.05003906 26.59667969]
    [ 0.79433594  0.23867188  0.25976562  0.05003906 26.59667969]
    [ 0.79433594  0.21835938  0.50742187  0.05003906 26.59667969]
    [ 0.79433594  0.21835938  0.25976562  0.07707031 26.59667969]
    [ 0.79433594  0.21835938  0.25976562  0.05003906 25.15136719]
    [ 0.79433594  0.21835938  0.25976562  0.05003906 26.59667969]
    [ 0.98183594  0.43867188  0.30742188  0.05707031 22.65136719]
    [ 1.09433594  0.43867188  0.30742188  0.05707031 22.65136719]
    [ 0.98183594  0.41835937  0.30742188  0.05707031 22.65136719]
    [ 0.98183594  0.43867188  0.45976563  0.05707031 22.65136719]
    [ 0.98183594  0.43867188  0.30742188  0.07003906 22.65136719]
    [ 0.98183594  0.43867188  0.30742188  0.05707031 24.09667969]
    [ 0.98183594  0.41835937  0.45976563  0.07003906 24.09667969]
    [ 1.09433594  0.43867188  0.45976563  0.07003906 24.09667969]
    [ 1.09433594  0.41835937  0.30742188  0.07003906 24.09667969]
    [ 1.09433594  0.41835937  0.45976563  0.05707031 24.09667969]
    [ 1.09433594  0.41835937  0.45976563  0.07003906 22.65136719]
    [ 1.09433594  0.41835937  0.45976563  0.07003906 24.09667969]
    [ 0.90683594  0.28867188  0.25742188  0.05207031 23.27636719]
    [ 1.16933594  0.28867188  0.25742188  0.05207031 23.27636719]
    [ 0.90683594  0.26835938  0.25742188  0.05207031 23.27636719]
    [ 0.90683594  0.28867188  0.20976563  0.05207031 23.27636719]
    [ 0.90683594  0.28867188  0.25742188  0.05503906 23.27636719]
    [ 0.90683594  0.28867188  0.25742188  0.05207031 25.97167969]
    [ 0.90683594  0.26835938  0.20976563  0.05503906 25.97167969]
    [ 1.16933594  0.28867188  0.20976563  0.05503906 25.97167969]
    [ 1.16933594  0.26835938  0.25742188  0.05503906 25.97167969]
    [ 1.16933594  0.26835938  0.20976563  0.05207031 25.97167969]
    [ 1.16933594  0.26835938  0.20976563  0.05503906 23.27636719]
    [ 1.16933594  0.26835938  0.20976563  0.05503906 25.97167969]
    [ 1.20683594  0.48867187  0.45742187  0.07207031 25.77636719]
    [ 0.86933594  0.48867187  0.45742187  0.07207031 25.77636719]
    [ 1.20683594  0.46835938  0.45742187  0.07207031 25.77636719]
    [ 1.20683594  0.48867187  0.40976563  0.07207031 25.77636719]
    [ 1.20683594  0.48867187  0.45742187  0.07503906 25.77636719]
    [ 1.20683594  0.48867187  0.45742187  0.07207031 23.47167969]
    [ 1.20683594  0.46835938  0.40976563  0.07503906 23.47167969]
    [ 0.86933594  0.48867187  0.40976563  0.07503906 23.47167969]
    [ 0.86933594  0.46835938  0.45742187  0.07503906 23.47167969]
    [ 0.86933594  0.46835938  0.40976563  0.07207031 23.47167969]
    [ 0.86933594  0.46835938  0.40976563  0.07503906 25.77636719]
    [ 0.86933594  0.46835938  0.40976563  0.07503906 23.47167969]
    [ 1.05683594  0.18867188  0.35742188  0.04207031 27.02636719]
    [ 0.71933594  0.18867188  0.35742188  0.04207031 27.02636719]
    [ 1.05683594  0.16835938  0.35742188  0.04207031 27.02636719]
    [ 1.05683594  0.18867188  0.50976562  0.04207031 27.02636719]
    [ 1.05683594  0.18867188  0.35742188  0.06503906 27.02636719]
    [ 1.05683594  0.18867188  0.35742188  0.04207031 24.72167969]
    [ 1.05683594  0.16835938  0.50976562  0.06503906 24.72167969]
    [ 0.71933594  0.18867188  0.50976562  0.06503906 24.72167969]
    [ 0.71933594  0.16835938  0.35742188  0.06503906 24.72167969]
    [ 0.71933594  0.16835938  0.50976562  0.04207031 24.72167969]
    [ 0.71933594  0.16835938  0.50976562  0.06503906 27.02636719]
    [ 0.71933594  0.16835938  0.50976562  0.06503906 24.72167969]
    [ 0.75683594  0.38867188  0.55742187  0.06207031 24.52636719]
    [ 1.01933594  0.38867188  0.55742187  0.06207031 24.52636719]
    [ 0.75683594  0.36835938  0.55742187  0.06207031 24.52636719]
    [ 0.75683594  0.38867188  0.30976562  0.06207031 24.52636719]
    [ 0.75683594  0.38867188  0.55742187  0.04503906 24.52636719]
    [ 0.75683594  0.38867188  0.55742187  0.06207031 27.22167969]
    [ 0.75683594  0.36835938  0.30976562  0.04503906 27.22167969]
    [ 1.01933594  0.38867188  0.30976562  0.04503906 27.22167969]
    [ 1.01933594  0.36835938  0.55742187  0.04503906 27.22167969]
    [ 1.01933594  0.36835938  0.30976562  0.06207031 27.22167969]
    [ 1.01933594  0.36835938  0.30976562  0.04503906 24.52636719]
    [ 1.01933594  0.36835938  0.30976562  0.04503906 27.22167969]
    [ 0.73808594  0.17617188  0.21992188  0.05832031 25.62011719]
    [ 0.85058594  0.17617188  0.21992188  0.05832031 25.62011719]
    [ 0.73808594  0.48085937  0.21992188  0.05832031 25.62011719]
    [ 0.73808594  0.17617188  0.52226562  0.05832031 25.62011719]
    [ 0.73808594  0.17617188  0.21992188  0.04128906 25.62011719]
    [ 0.73808594  0.17617188  0.21992188  0.05832031 24.56542969]
    [ 0.73808594  0.48085937  0.52226562  0.04128906 24.56542969]
    [ 0.85058594  0.17617188  0.52226562  0.04128906 24.56542969]
    [ 0.85058594  0.48085937  0.21992188  0.04128906 24.56542969]
    [ 0.85058594  0.48085937  0.52226562  0.05832031 24.56542969]
    [ 0.85058594  0.48085937  0.52226562  0.04128906 25.62011719]
    [ 0.85058594  0.48085937  0.52226562  0.04128906 24.56542969]
    [ 1.03808594  0.37617188  0.41992188  0.07832031 23.12011719]
    [ 1.15058594  0.37617188  0.41992188  0.07832031 23.12011719]
    [ 1.03808594  0.28085938  0.41992188  0.07832031 23.12011719]
    [ 1.03808594  0.37617188  0.32226562  0.07832031 23.12011719]
    [ 1.03808594  0.37617188  0.41992188  0.06128906 23.12011719]
    [ 1.03808594  0.37617188  0.41992188  0.07832031 27.06542969]
    [ 1.03808594  0.28085938  0.32226562  0.06128906 27.06542969]
    [ 1.15058594  0.37617188  0.32226562  0.06128906 27.06542969]
    [ 1.15058594  0.28085938  0.41992188  0.06128906 27.06542969]
    [ 1.15058594  0.28085938  0.32226562  0.07832031 27.06542969]
    [ 1.15058594  0.28085938  0.32226562  0.06128906 23.12011719]
    [ 1.15058594  0.28085938  0.32226562  0.06128906 27.06542969]
    CPU times: user 1min 55s, sys: 702 ms, total: 1min 56s
    Wall time: 2min 1s

```python
np_yield = np.array(yield_list)
```

```python
np_yield.shape
```

    (120,)

```python
Si = sobol.analyze(Soil_parameters, np_yield, print_to_console=False)
Si_df = Si.to_df()
Si_df
```

    [              ST   ST_conf
     SMV     0.000000  0.000000
     SMFCF   0.433048  0.885393
     SM0     0.377433  0.568678
     CRAIRC  0.000000  0.000000
     K0      0.000000  0.000000,
                   S1   S1_conf
     SMV     0.000000  0.000000
     SMFCF  -0.125075  0.924747
     SM0     0.256621  0.566706
     CRAIRC  0.000000  0.000000
     K0      0.000000  0.000000,
                                S2       S2_conf
     (SMV, SMFCF)     1.110223e-16  2.159656e-16
     (SMV, SM0)      -5.551115e-17  1.072992e-16
     (SMV, CRAIRC)    0.000000e+00  0.000000e+00
     (SMV, K0)        0.000000e+00  0.000000e+00
     (SMFCF, SM0)     1.651238e-01  1.386681e+00
     (SMFCF, CRAIRC)  1.258354e-01  1.375128e+00
     (SMFCF, K0)      1.258354e-01  1.375128e+00
     (SM0, CRAIRC)    4.283364e-01  1.090745e+00
     (SM0, K0)        4.283364e-01  1.090745e+00
     (CRAIRC, K0)     0.000000e+00  0.000000e+00]

## Visualizing simulation results

Finally, we can generate some figures of WOFOST variables such as the development (DVS), total biomass (TAGP), leaf area index (LAI) and root-zone soil moisture (SM) using the MatPlotLib plotting package:

```python
fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(12,10))
for var, ax in zip(["DVS", "TWSO", "LAI", "SM"], axes.flatten()):
    ax.plot_date(df_results.index, df_results[var], 'b-')
    ax.set_title(var)
fig.autofmt_xdate()
```

# Sensitivity analysis

In this example, we will perform a Sobol’ sensitivity analysis of the Ishigami function, shown below. The Ishigami function is commonly used to test uncertainty and sensitivity analysis methods because it exhibits strong nonlinearity and nonmonotonicity.

$f(x)=\sin \left(x_{1}\right)+ \text{a}\, \operatorname{sin}^{2}\left(x_{2}\right)+ \text{b}\,  x_{3}^{4} \sin \left(x_{1}\right)$

```python
from SALib.sample import saltelli
from SALib.analyze import sobol
from SALib.test_functions import Ishigami
import numpy as np
```

Next, we must define the model inputs. The Ishigami function has three inputs, $x_{1}$,$x_{2}$,$x_{3}$ where $x_{i} \in[-\pi, \pi]$. In SALib, we define a `dict` defining the number of inputs, the names of the inputs, and the bounds on each input, as shown below.

```python
problem = {
  'num_vars': 3,
  'names': ['x1', 'x2', 'x3'],
  'bounds': [[-np.pi, np.pi]]*3
}
```

```python
for j, i in enumerate(list_of_csv):
        all_data_csv = genfromtxt('./'+str(i), delimiter=',')
        output = all_data_csv[:,2]
        print(i)
        list_of_SI[j] = sobol.analyze(problem, output, calc_second_order=True, conf_level=0.95, print_to_console=False)

```

Here, `param_values` is a NumPy matrix. If we run `param_values.shape`, we see that the matrix is **8000 by 3**. The Saltelli sampler generated 8000 samples. The Saltelli sampler generates $N∗(2D+2)$ samples, where in this example $N$ is 1000 (the argument we supplied) and $D$ is 3 (the number of model inputs). The keyword argument `calc_second_order=False` will exclude second-order indices, resulting in a smaller sample matrix with $N∗(D+2)$ rows instead.

```python
# Generate samples
param_values = saltelli.sample(problem, 1000)

# Run model (example)
Y = Ishigami.evaluate(param_values)

# Perform analysis
Si = sobol.analyze(problem, Y, print_to_console=True)
# Returns a dictionary with keys 'S1', 'S1_conf', 'ST', and 'ST_conf'
# (first and total-order indices with bootstrap confidence intervals)
T_Si, first_Si, (idx, second_Si) = sobol.Si_to_pandas_dict(Si)
```

    Parameter S1 S1_conf ST ST_conf
    x1 0.307975 0.063769 0.560137 0.084886
    x2 0.447767 0.056001 0.438722 0.040663
    x3 -0.004255 0.058466 0.242845 0.024166

    Parameter_1 Parameter_2 S2 S2_conf
    x1 x2 0.012205 0.088213
    x1 x3 0.251526 0.106417
    x2 x3 -0.009954 0.067973

## Simple tool for Sobol' seq and LHS sample visualization for sensitivity analysis

How to repeat: [Repo of SampleVIS](https://github.com/charlesrouge/SampleVis)

Plots by [Water programming group](https://waterprogramming.wordpress.com/2019/08/27/a-python-implementation-of-grouped-radial-convergence-plots-to-visualize-sobol-sensitivity-analysis-results/)

```python
import numpy as np
from SALib.analyze import sobol
from SALib.sample import saltelli
from fishery import fish_game
import matplotlib.pyplot as plt
import itertools
import math
```

### Why number of samples is important?

```python
# Set up dictionary with system parameters
problem = {
  'num_vars': 6,
  'names': ['a', 'b', 'c','h',
            'K','m'],
  'bounds': [[ 0.002, 2],
             [0.005, 1],
             [0.2, 1],
             [0.001, 1],
             [100, 5000],
             [0.1, 1.5]]
}

# Array with n's to use
nsamples = np.arange(50, 500, 50)

# Arrays to store the index estimates
S1_estimates = np.zeros([problem['num_vars'],len(nsamples)])
ST_estimates = np.zeros([problem['num_vars'],len(nsamples)])

# Loop through all n values, create sample, evaluate model and estimate S1 & ST
for i in range(len(nsamples)):
    print('n= '+ str(nsamples[i]))
    # Generate samples
    sampleset = saltelli.sample(problem, nsamples[i],calc_second_order=False)
    # Run model for all samples
    output = [fish_game(*sampleset[j,:]) for j in range(len(sampleset))]
    # Perform analysis
    results = sobol.analyze(problem, np.asarray(output), calc_second_order=False,print_to_console=False)
    # Store estimates
    ST_estimates[:,i]=results['ST']
    S1_estimates[:,i]=results['S1']

np.save('ST_estimates.npy', ST_estimates)
np.save('S1_estimates.npy', S1_estimates)

S1_estimates = np.load('S1_estimates.npy')
ST_estimates = np.load('ST_estimates.npy')

# Generate figure showing evolution of indices
fig =  plt.figure(figsize=(18,9))
ax1 = fig.add_subplot(1,2,1)
handles = []
for j in range(problem['num_vars']):
    handles += ax1.plot(nsamples, S1_estimates[j,:], linewidth=5)
ax1.set_title('Evolution of S1 index estimates', fontsize=20)
ax1.set_ylabel('S1', fontsize=18)
ax1.set_xlabel('Number of samples (n)', fontsize=18)
ax1.tick_params(axis='both', which='major', labelsize=14)
ax2 = fig.add_subplot(1,2,2)
for j in range(problem['num_vars']):
    ax2.plot(nsamples, ST_estimates[j,:], linewidth=5)
ax2.set_title('Evolution of ST index estimates', fontsize=20)
ax2.set_ylabel('ST', fontsize=18)
ax2.tick_params(axis='both', which='major', labelsize=14)
ax2.set_xlabel('Number of samples (n)', fontsize=18)
fig.legend(handles, problem['names'], loc = 'right', fontsize=11)
plt.savefig('indexevolution.png')

# Calculate parameter rankings
S1_ranks = np.zeros_like(S1_estimates)
ST_ranks = np.zeros_like(ST_estimates)
for i in range(len(nsamples)):
    orderS1 = np.argsort(S1_estimates[:,i])
    orderST = np.argsort(ST_estimates[:,i])
    S1_ranks[:,i] = orderS1.argsort()
    ST_ranks[:,i] = orderST.argsort()

# Generate figure showing evolution of ranks
fig =  plt.figure(figsize=(18,9))
ax1 = fig.add_subplot(1,2,1)
handles = []
for j in range(problem['num_vars']):
    handles += ax1.plot(nsamples, S1_ranks[j,:], linewidth=3)
ax1.set_title('Parameter ranking based on S1', fontsize=20)
ax1.set_ylabel('S1', fontsize=18)
ax1.set_xlabel('Number of samples (n)', fontsize=18)
ax1.set_yticklabels(np.arange(problem['num_vars']+1, 0, -1))
ax1.tick_params(axis='both', which='major', labelsize=14)
ax2 = fig.add_subplot(1,2,2)
for j in range(problem['num_vars']):
    ax2.plot(nsamples, ST_ranks[j,:], linewidth=3)
ax2.set_title('Parameter ranking based on ST', fontsize=20)
ax2.set_ylabel('ST', fontsize=18)
ax2.set_yticklabels(np.arange(problem['num_vars']+1, 0, -1))
ax2.tick_params(axis='both', which='major', labelsize=14)
ax2.set_xlabel('Number of samples (n)', fontsize=18)
fig.legend(handles, problem['names'], loc = 'right', fontsize=14)
plt.show()
#plt.savefig('rankingevolution.png')
# n is sample size for sobol' seq
```

# Radial plot for SA

```python
import numpy as np
import itertools
import matplotlib.pyplot as plt
import seaborn as sns
import math
from numpy import genfromtxt
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
sns.set_style('whitegrid', {'axes_linewidth': 0, 'axes.edgecolor': 'white'})
```

```python
from RadialPlot import grouped_radial
from RadialPlot import is_significant
```

```python
def is_significant(value, confidence_interval, threshold="conf"):
    if threshold == "conf":
        return value - abs(confidence_interval) > 0
    else:
        return value - abs(float(threshold)) > 0

def grouped_radial(SAresults, parameters, radSc=2.0, scaling=1, widthSc=0.5, STthick=1, varNameMult=1.3, colors=None, groups=None, gpNameMult=1.5, threshold="conf"):
    # Derived from https://github.com/calvinwhealton/SensitivityAnalysisPlots
    fig, ax = plt.subplots(1, 1)
    color_map = {}

    # initialize parameters and colors
    if groups is None:

        if colors is None:
            colors = ["k"]

        for i, parameter in enumerate(parameters):
            color_map[parameter] = colors[i % len(colors)]
    else:
        if colors is None:
            colors = sns.color_palette("deep", max(3, len(groups)))

        for i, key in enumerate(groups.keys()):
            #parameters.extend(groups[key])

            for parameter in groups[key]:
                color_map[parameter] = colors[i % len(colors)]

    n = len(parameters)
    angles = radSc*math.pi*np.arange(0, n)/n
    x = radSc*np.cos(angles)
    y = radSc*np.sin(angles)

    # plot second-order indices
    for i, j in itertools.combinations(range(n), 2):
        #key1 = parameters[i]
        #key2 = parameters[j]

        if is_significant(SAresults["S2"][i][j], SAresults["S2_conf"][i][j], threshold):
            angle = math.atan((y[j]-y[i])/(x[j]-x[i]))

            if y[j]-y[i] < 0:
                angle += math.pi

            line_hw = scaling*(max(0, SAresults["S2"][i][j])**widthSc)/2

            coords = np.empty((4, 2))
            coords[0, 0] = x[i] - line_hw*math.sin(angle)
            coords[1, 0] = x[i] + line_hw*math.sin(angle)
            coords[2, 0] = x[j] + line_hw*math.sin(angle)
            coords[3, 0] = x[j] - line_hw*math.sin(angle)
            coords[0, 1] = y[i] + line_hw*math.cos(angle)
            coords[1, 1] = y[i] - line_hw*math.cos(angle)
            coords[2, 1] = y[j] - line_hw*math.cos(angle)
            coords[3, 1] = y[j] + line_hw*math.cos(angle)

            ax.add_artist(plt.Polygon(coords, color="0.75"))

    # plot total order indices
    for i, key in enumerate(parameters):
        if is_significant(SAresults["ST"][i], SAresults["ST_conf"][i], threshold):
            ax.add_artist(plt.Circle((x[i], y[i]), scaling*(SAresults["ST"][i]**widthSc)/2, color='w'))
            ax.add_artist(plt.Circle((x[i], y[i]), scaling*(SAresults["ST"][i]**widthSc)/2, lw=STthick, color='0.4', fill=False))

    # plot first-order indices
    for i, key in enumerate(parameters):
        if is_significant(SAresults["S1"][i], SAresults["S1_conf"][i], threshold):
            ax.add_artist(plt.Circle((x[i], y[i]), scaling*(SAresults["S1"][i]**widthSc)/2, color='0.4'))

    # add labels
    for i, key in enumerate(parameters):
        ax.text(varNameMult*x[i], varNameMult*y[i], key, ha='center', va='center',
                rotation=angles[i]*360/(2*math.pi) - 90,
                color=color_map[key])

    if groups is not None:
        for i, group in enumerate(groups.keys()):
            print(group)
            group_angle = np.mean([angles[j] for j in range(n) if parameters[j] in groups[group]])

            ax.text(gpNameMult*radSc*math.cos(group_angle), gpNameMult*radSc*math.sin(group_angle), group, ha='center', va='center',
                rotation=group_angle*360/(2*math.pi) - 90,
                color=colors[i % len(colors)])

    ax.set_facecolor('white')
    ax.set_xticks([])
    ax.set_yticks([])
#     ax.
    plt.axis('equal')
    plt.axis([-2*radSc, 2*radSc, -2*radSc, 2*radSc])
    #plt.show()

    return fig
```

### Range of soil parameters

```python
problem = {
    'num_vars':6,
    'names':['SOC', 'Sand', 'Clay', 'pH', 'CN', 'BD'],
    'bounds':[[2.58, 6.20],
              [0.01, 0.30],
              [0.01, 0.30],
              [4.6, 6.9],
              [10.9, 12.4],
              [900, 1350]]
}
```

```python
#names for csv files
list_of_csv=['soybean-000-2015.csv', 'sugar-beet-2011.csv', 'sugar-beet-2017.csv',
'spring-barley-2012.csv', 'sugar-beet-2014.csv']
list_of_names=['soybean-000-2015', 'sugar-beat-2011', 'sugar-beat-2017',
'spring-barley-2012', 'sugar-beat-2014']
list_of_totals=['total_SI_'+x for x in list_of_names]
list_of_first=['fisrt_SI_'+x for x in list_of_names]
list_of_second=['second_SI_'+x for x in list_of_names]
list_of_SI=['SI_'+x for x in list_of_names]
```

```python
for j, i in enumerate(list_of_csv):
#     for j in range(len(list_of_csv)):
        all_data_csv = genfromtxt('./'+str(i), delimiter=',')
        output = all_data_csv[:,2]
        print(i)
        list_of_SI[j] = sobol.analyze(problem, output, calc_second_order=True, conf_level=0.95, print_to_console=False)
#         list_of_totals[j], list_of_first[j], list_of_second[j] = Si.to_df()
#         str(i)+'total_Si'=total_Si
```

    soybean-000-2015.csv
    sugar-beet-2011.csv
    sugar-beet-2017.csv
    spring-barley-2012.csv
    sugar-beet-2014.csv

```python
groups={"Soil physics" : ["Sand", "Clay", "BD"],
        "Soil chemistry" : ["pH", "SOC", "CN"]}

fig = grouped_radial(list_of_SI[1], ['BD', 'Sand', 'Clay', 'pH', 'CN', 'SOC'], groups=groups, threshold=0.001)
red_patch = mpatches.Patch(color='red', label='The red data')
plt.title(list_of_names[3], loc='left')

plt.show()

# plt.savefig('grafik.png', dpi=300)
```

## Homework

Usefull

__SA and UQ__

1) [Rhodium project](https://github.com/Project-Platypus/Rhodium.git)

2) [SALib](https://github.com/SALib/SALib)

__Model__

3) [PCSE](https://pcse.readthedocs.io/en/stable/index.html)

### bonus

Generate a sample using the Method of Morris

Three variants of Morris' sampling for elementary effects is supported:

- Vanilla Morris
- Optimised trajectories when ``optimal_trajectories=True`` (using
    Campolongo's enhancements from 2007 and optionally Ruano's enhancement
    from 2012; ``local_optimization=True``)
- Groups with optimised trajectories when ``optimal_trajectories=True`` and
    the problem definition specifies groups (note that ``local_optimization``
    must be ``False``)

At present, optimised trajectories is implemented using either a brute-force
approach, which can be very slow, especially if you require more than four
trajectories, or a local method based which is much faster. Both methods now
implement working with groups of factors.

Note that the number of factors makes little difference,
but the ratio between number of optimal trajectories and the sample size
results in an exponentially increasing number of scores that must be
computed to find the optimal combination of trajectories.  We suggest going
no higher than 4 from a pool of 100 samples with the brute force approach.
With local_optimization = True (which is default),
it is possible to go higher than the previously suggested 4 from 100.

```python
import sys

from SALib.analyze import morris
from SALib.sample.morris import sample
from SALib.test_functions import Sobol_G
from SALib.util import read_param_file
from SALib.plotting.morris import horizontal_bar_plot, covariance_plot, \
    sample_histograms
import matplotlib.pyplot as plt

#sys.path.append('../..')

# Read the parameter range file and generate samples
#problem = read_param_file('/Users/mikhailgasanov/Documents/GIT/SALib/src/SALib/test_functions/params/Sobol_G.txt')
# or define manually without a parameter file:
problem = {
 'num_vars': 8,
 'names': ['x1', 'x2', 'x3', 'x4', 'x5', 'x6', 'x7', 'x8'],
 'groups': None,
 'bounds': [[0.0, 1.0],
            [0.0, 1.0],
            [0.0, 1.0],
            [0.0, 1.0],
            [0.0, 1.0],
            [0.0, 1.0],
            [0.0, 1.0],
            [0.0, 1.0]]
}
# Files with a 4th column for "group name" will be detected automatically, e.g.
# param_file = '../../src/SALib/test_functions/params/Ishigami_groups.txt'

# Generate samples
param_values = sample(problem, N=1000, num_levels=4,
                      optimal_trajectories=None)

# To use optimized trajectories (brute force method),
# give an integer value for optimal_trajectories

# Run the "model" -- this will happen offline for external models
Y = Sobol_G.evaluate(param_values)

# Perform the sensitivity analysis using the model output
# Specify which column of the output file to analyze (zero-indexed)
Si = morris.analyze(problem, param_values, Y, conf_level=0.95,
                    print_to_console=True,
                    num_levels=4, num_resamples=100)
# Returns a dictionary with keys 'mu', 'mu_star', 'sigma', and 'mu_star_conf'
# e.g. Si['mu_star'] contains the mu* value for each parameter, in the
# same order as the parameter file

fig, (ax1, ax2) = plt.subplots(1, 2)
horizontal_bar_plot(ax1, Si, {}, sortby='mu_star', unit=r"tCO$_2$/year")
covariance_plot(ax2, Si, {}, unit=r"tCO$_2$/year")

fig2 = plt.figure()
sample_histograms(fig2, param_values, problem, {'color': 'y'})
plt.show()
```

```python

```