```latex
%%latex

Naive Bayes - Bayers Theorem implemented
$$\\$$
\begin{equation}
P(A\mid B) = \frac{P(B\mid A)P(A)}{P(B)}
\end{equation}
```

Naive Bayes - Bayers Theorem implemented
$$\\$$
\begin{equation}
P(A\mid B) = \frac{P(B\mid A)P(A)}{P(B)}
\end{equation}

```python
import pandas            as pd
import numpy             as np
import matplotlib.pyplot as plt
%matplotlib inline

from sklearn.naive_bayes import GaussianNB

from sklearn.model_selection  import  train_test_split
```

```python
dataColumns = 'A','B','C','D','E','F','G','H','label'

data_read = pd.read_csv('pima-indians-diabetes.csv',header = None, names = dataColumns)

x = data_read.iloc[:,0:8]
y = data_read.iloc[:,8:10]

```

```python
x_train, x_test, y_train, y_test = train_test_split(x,y,test_size=0.33,random_state=101)

x_treinamento = x_train.as_matrix()
y_treinamento = y_train.as_matrix().ravel()
x_teste       = x_test.as_matrix()
y_teste       = y_test.as_matrix().ravel()

#y_treinamento= y_treinamento.ravel()

```

```python
#Create a Gaussian Classifier
model = GaussianNB()

# Train the model using the training sets
model.fit(x_treinamento,y_treinamento)
```

    GaussianNB(priors=None)

```python
predicted= model.predict(x_teste)
print(predicted)
```

    [0 1 0 1 0 1 1 0 0 0 0 0 0 1 0 1 1 0 0 0 0 0 1 0 0 1 0 1 0 1 1 0 0 1 0 0 0
     1 1 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0 1 0 0 0
     1 1 0 1 0 1 1 0 1 1 1 0 0 0 1 1 1 0 1 0 0 0 0 0 1 1 0 0 0 0 0 0 0 1 0 0 1
     0 0 0 1 1 0 0 0 0 1 0 0 1 1 1 0 1 0 0 0 1 0 1 0 0 0 0 0 0 0 1 1 1 0 0 1 0
     1 1 0 0 1 1 0 1 0 0 0 0 0 0 0 1 0 0 0 1 1 0 0 1 0 1 0 0 0 0 1 0 1 1 0 1 0
     0 0 1 1 1 1 0 0 0 0 0 0 0 0 1 0 0 1 0 0 1 1 1 1 1 0 1 0 0 0 0 0 0 0 0 0 0
     1 0 0 1 0 1 1 0 1 0 1 0 0 1 0 1 1 0 0 0 0 1 0 1 1 0 0 0 0 0 1 0]

```python
sum((y_teste-predicted)**2)
```

    65

```python
y_teste
```

    array([1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1,
           1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0,
           0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0,
           0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0,
           1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1,
           0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0,
           0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0,
           0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1,
           0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0,
           1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0,
           1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0,
           1])


# Beginners' tutorial: Bayesian optimisation & modelling with Pints

**Prerequisites** for running the code in this notebook are Python 3 with the modules below. All of the modules used are available from PyPI by typing `pip install <module name here>`. The exception is Pints, which is available from [this github repository](https://github.com/pints-team/pints) by following the instructions in the readme.

```python
import sys
!{sys.executable} -m pip install ./pints-master
```

    Processing ./pints-master
    Requirement already satisfied: cma>=2 in /home/rumneyr/anaconda3/lib/python3.7/site-packages (from pints==0.2.2) (3.0.3)
    Requirement already satisfied: numpy>=1.8 in /home/rumneyr/anaconda3/lib/python3.7/site-packages (from pints==0.2.2) (1.17.2)
    Requirement already satisfied: scipy>=0.14 in /home/rumneyr/anaconda3/lib/python3.7/site-packages (from pints==0.2.2) (1.3.1)
    Requirement already satisfied: matplotlib>=1.5 in /home/rumneyr/anaconda3/lib/python3.7/site-packages (from pints==0.2.2) (3.1.1)
    Requirement already satisfied: tabulate in /home/rumneyr/anaconda3/lib/python3.7/site-packages (from pints==0.2.2) (0.8.7)
    Requirement already satisfied: cycler>=0.10 in /home/rumneyr/anaconda3/lib/python3.7/site-packages (from matplotlib>=1.5->pints==0.2.2) (0.10.0)
    Requirement already satisfied: kiwisolver>=1.0.1 in /home/rumneyr/anaconda3/lib/python3.7/site-packages (from matplotlib>=1.5->pints==0.2.2) (1.1.0)
    Requirement already satisfied: pyparsing!=2.0.4,!=2.1.2,!=2.1.6,>=2.0.1 in /home/rumneyr/anaconda3/lib/python3.7/site-packages (from matplotlib>=1.5->pints==0.2.2) (2.4.2)
    Requirement already satisfied: python-dateutil>=2.1 in /home/rumneyr/anaconda3/lib/python3.7/site-packages (from matplotlib>=1.5->pints==0.2.2) (2.8.0)
    Requirement already satisfied: six in /home/rumneyr/anaconda3/lib/python3.7/site-packages (from cycler>=0.10->matplotlib>=1.5->pints==0.2.2) (1.12.0)
    Requirement already satisfied: setuptools in /home/rumneyr/anaconda3/lib/python3.7/site-packages (from kiwisolver>=1.0.1->matplotlib>=1.5->pints==0.2.2) (41.4.0)
    Building wheels for collected packages: pints
      Building wheel for pints (setup.py) ... done
      Created wheel for pints: filename=pints-0.2.2-cp37-none-any.whl size=229011 sha256=d58dee2ce39202199d2f4c7532fed4aa2ce593f39b982240b83f9c67aeb78649
      Stored in directory: /home/rumneyr/.cache/pip/wheels/23/f1/0b/7f6d8d6d6358237dd5757166c40f8f414a896a8f6f3b677add
    Successfully built pints
    Installing collected packages: pints
      Found existing installation: pints 0.2.2
        Uninstalling pints-0.2.2:
          Successfully uninstalled pints-0.2.2
    Successfully installed pints-0.2.2

```python
import numpy as np
import math
import matplotlib.pyplot as plt
import scipy
from scipy import optimize, integrate
import pints
```

# Table of Contents
1. [**Bayesian probability revision**](#probability)
1. [**Bayesian optimisation**](#optimisation)
1. [**Bayesian sampling/MCMC**](#sampling)
1. [**Hierarchical MCMC**](#hierarchical)

# Bayesian probability revision

This notebook uses the following naming conventions:-
**$y$:** Measured output from the system (or $y_{model}$, the model output)
**$\theta$, theta:** Model parameters (in our example, $\theta=k$)
**$k$:** Decay constant (the single parameter in our example model)
**$\sigma$, sigma:** Variance of the measurement error

We want to find $P(\theta|y)$ (the posterior probability), which is  the probability distribution on the left hand side of Bayes' rule (equation 1, below).
\begin{equation}
P(\theta|y) = \frac{P(y|\theta) P(\theta)}{P(y)}
\tag{1}
\end{equation}

For our example, we are using a Gaussian probability distribution (i.e. normal distribution) to compare to measured data, so $P(X) = \frac{1}{\sqrt{2\pi\sigma^2}}e^{\frac{-(X-data)^2}{2\sigma^2}}$. This seems reasonable because random noise often behaves in a roughly Gaussian way. We decide on a uniform prior between 0 and 10, to show that before analysis we believe that $k$ could equally be any value between 0 and 10. Therefore we can calculate the numerator of Bayes' rule as follows.

\begin{equation}
\text{Numerator} = P(y|\theta) P(\theta) = \frac{1}{\sqrt{2\pi\sigma^2}}e^{\frac{-(y-\theta)^2}{2\sigma^2}} \cdot U(0,10)
\tag{2}
\end{equation}

This is what we'll use throughout to calculate $P(\theta|y)$, the probability of any given parameter set $\theta$.

# Bayesian optimisation

In optimisation, you measure the output of a system with a known input, compare it to the measured output of the 'real' system, and so estimate the parameters most likely to give that output.

For pharmacokinetics, the system of interest is the distribution of a drug in the body. The input is some dose, and the system's output is concentration of the drug, measured in a particular compartment at several points in time. In this example we use a one-compartment model, which only has one parameter in the model, labelled $k$ here. The output, $y$, is defined by the ODE:-
\begin{equation}
\frac{d y}{d t} = -k \cdot y
\tag{3}
\end{equation}

```python
# Parameters
k = 1.5
times = np.linspace(0,10,50)

# A one-compartment PK model is basically an ODE for an exponential decay curve
def onecomp(y, t, k):
    """A one-compartment PK model, aka simple exponential decay
    In
    y: y-axis value
    t: x-axis value
    k: gradient parameter

    Out: gradient at the given point
    """
    dydt = -k * y
    return dydt

# You can solve ODEs with the Forward Euler method
def ForwardEuler(func, k, y0, times):
    """Numerically calculate an ODE (forward Euler technique)
    In
    func: function giving the derivative
    k: parameters the function requires as input
    y0: y at t=0
    times: array of times at which to calculate y

    Out: array of y values at corresponding points in times
    """
    y = y0
    y_solution = [y0]
    h = times[2] - times[1]

    for n in times[:-1]:
        dy = func(y, n, k)
        y = y + h*dy
        y_solution.append(y)

    return y_solution

# You can also solve ODEs with scipy.integrate.odeint
def simulate(func, parameters, y0, times):
    """Numerically calculate an ODE
    In
    func: function giving the derivative
    parameters: parameters the function requires as input
    y0: y at t=0
    times: array of times at which to calculate y

    Out: array of y values at corresponding points in times
    """
    l = scipy.integrate.odeint(func, y0, times, (parameters,)) # returns a list of lists
    flatlist = [item for sublist in l for item in sublist] # to single list of 'y's
    return flatlist

# This is what the system output looks like. We don't actually know these values
actual_values_euler = ForwardEuler(onecomp, k, 1, times)
actual_values = simulate(onecomp, k, 1, times)

# Plot the model results
plt.figure()
plt.xlabel('Time')
plt.ylabel('Concentration')
plt.plot(times, actual_values, '--', label='$ dy/dt = k \cdot y$')
#plt.plot(times, actual_values_euler, label='forward Euler')
plt.legend()
plt.show()
```

Without prior knowledge, we can make a guess at a model type that would give this kind of data, but we don't know what the parameters of the model are. We usually call these parameters theta ($\theta$) and in this single-compartment case the only parameter is k.

While we can estimate concentration (y(t)) at a given time (t) numerically by plotting from y(0), we have to guess a value of k. **We are trying to find the true value of k**.

You could measure from the system we're modelling, and you would get measurements with some measurement error, which we're modelling as gaussian noise below.

```python
# Make noisy data that we're inferring from. noisy_data is known to us.
noise = np.random.normal(0, 0.03, len(actual_values))
noisy_data = actual_values + noise
plt.figure()
plt.plot(times, noisy_data, '.', label='Measured values (we know these)')
plt.plot(times, actual_values, label='Actual values (we don\'t know these)')
plt.xlabel('Time')
plt.ylabel('Concentration')
plt.legend()
plt.show()
```

We're trying to find which value of $k$ makes our model output the closest to the real system output, assuming that the input dose for each is the same. To do this we define a function, `scalar_to_minimise`, whose output reduced in size, the closer model output is to measured system output.

```python
# So what do we want to minimise?
def sumofsquares(y_model, y_data):
    """Gives the sum of the square of all errors between model and experimental data.
    In
    y_model: list of output values from model
    y_data: list of experimental (i.e. noisy) values

    Out: the sum of square error
    """
    sq_error = []
    for t in range(len(y_model)):
        sq_error.append((y_data[t] - y_model[t])**2)

    return sum(sq_error)
```

```python
# Optimise it with scipy
def scalar_to_minimise(parameters):
    """For a one compartment model & sum of squares this is what's minimised"""
    y_model = simulate(onecomp, parameters, 1, times)
    y_data = noisy_data
    return sumofsquares(y_model, y_data) / len(y_model)

start_params = 11
result = scipy.optimize.minimize_scalar(scalar_to_minimise)
print('Calculated k: \t'+str(result.x))
print('Real k: \t'+str(k))
```

    Calculated k: 	1.4613578865952117
    Real k: 	1.5

```python
# What does that look like?
recon_model = simulate(onecomp, result.x, 1, times)
plt.figure()
plt.plot(times, noisy_data, '.', label='Measured values')
plt.plot(times, actual_values, label='Actual values')
plt.plot(times, recon_model, '--', label='Inferred values')
plt.xlabel('Time')
plt.ylabel('Concentration')
plt.legend()
plt.show()
```

## Using Pints
As you can see, our estimator estimates $k$ pretty well simply based on minimising the output of `scalar_to_minimise`. So how would we do what we've done above with Pints? Well, it's actually both easier to code and faster to calculate.

```python
# Need to make the model into a Pints one
class PintsOneComp(pints.ForwardModel):
    def n_parameters(self):
        return 1
    def simulate(self, parameter, times):
        return ForwardEuler(onecomp, parameter, 1, times)

problem = pints.SingleOutputProblem(PintsOneComp(), times, noisy_data)
error_measure = pints.SumOfSquaresError(problem)
optimisation = pints.OptimisationController(error_measure, [1], method=pints.XNES)
optimisation.set_log_to_screen(False)
parameters, error = optimisation.run()

# How does it look?
print('Custom calculation result: \t'+str(result.x))
print('Pints calculation result: \t'+str(parameters[0]))
plt.figure()
plt.plot(times, noisy_data, '.', label='Measured values')
plt.plot(times, recon_model, '--', label='Custom inferred values')
plt.plot(times, PintsOneComp().simulate(parameters, times), '--', lw=2, label='Pints inferred values')
plt.legend()
plt.show()
```

# Bayesian sampling

Optimisation gives us the most likely parameters, but sampling can give a probability distribution of different parameter values. The method we'll use here is Metropolis Hastings. In our exponential decay model, our only model parameter ($\theta$) is $k$ (the decay constant in equation 3 at the start of this tutorial).

Metropolis Hastings creates a series of potential values of $\theta$ by making random proposals for new $\theta$s and calculating the numerator of Bayes' rule (equation 2) for each one.

**Steps for Metropolis Hastings**
1. Start with an arbitrary $\theta$ (a k and a $\sigma$). Calculate prior x likelihood.
2. Move to a different $\theta$ (+/- an amount from a normal distribution with SD given by the 'covariate matrix'). Calculate prior x likelihood.
3. If new prior x likelihood is higher than old, keep it and add the new $\theta$ to a list. If not, keep it if rand(0->1) > (old/new). If neither of these work, move back to the old one and add that instead.
4. Repeat steps 2-3 for N steps.
5. Count your list of $\theta$s into bins and draw a histogram. This approximates your posterior.

```python
# This model stores all the model-related variables, for simplicity.

class OdeModel():

    def __init__(self, thetas, covariates, prior, likelihood, modeltype):
        self.thetas = thetas
        self.covariates = covariates
        self.modeltype = modeltype
        self.prior = prior
        self.likelihood = likelihood
```

```python
# This gives a uniform prior P(theta) ~ U(0,10)
# i.e. for any input between 0 & 10, it outputs 0.1, else 0.

def uniform_prior(theta):
    """Returns 0.1 if entire input list is between 0 & 10, else 0"""

    prior = []
    for key, param in theta.items():
        if param > 0 and param < 10:
            prior.append(0.1)
        else:
            prior.append(0)

    return min(prior)
```

```python
# This calculates the likelihood P(y|theta)
# (probability that theta could give the output y_data)
# i.e. for input theta, the function returns:-
# 1/sqrt(2*pi*sigma) * exp(error^2/(2*sigma^2))

def likelihood_k(theta, y_data):
    """Returns the likelihood, P(theta|y)"""
    k = theta['k']
    sigma = 0.03
    pdf = []
    y_model = ForwardEuler(onecomp, k, 1, times)
    other_bit = 1/(2*math.pi*sigma**2)
    for t in range(len(y_data)): # this loop gives a normally distributed pdf
        square_error = (y_data[t] - y_model[t])**2
        exponential = math.exp(-square_error/(2*sigma**2))
        pdf.append(exponential*other_bit)

    return np.prod(pdf)
```

```python
# This does steps 2 & 3 in the instructions above.
# It takes the model, the current theta value, and measured data
# It returns an accepted theta value.
# This is proposed_theta if accepted, else theta

def propose_new_theta(model, y_data, theta):
    """Randomly proposes a new theta and decides whether to accept or not
    In
    model: instance of OdeModel class
    y_data: list with experimental data
    theta: parameters, in a list

    Out: new parameters, either the same (if proposed not accepted) or different
    """

    numerator = model.prior(theta) * model.likelihood(theta, y_data)

    # randomly get a proposed theta & calculate its numerator
    proposed_theta = {}
    for key, value in theta.items():
        proposed_k = np.random.normal(value, model.covariates[key])
        proposed_theta[key] = proposed_k
    proposed_numerator = model.prior(proposed_theta) * model.likelihood(proposed_theta, y_data)

    # if the new numerator should be accepted (metropolis hastings criteria), replace theta
    if proposed_numerator == 0:
        pass
    elif proposed_numerator > numerator:
        theta = proposed_theta
        numerator = proposed_numerator
    elif np.random.rand() < proposed_numerator/numerator:
        theta = proposed_theta
        numerator = proposed_numerator

    return theta

```

```python
# This just runs propose_new_theta repeatedly

def metropolis_singlethread(model, y_data, threadnum, max_iters):
    iters = 0

    while iters < max_iters:
        theta = propose_new_theta(model, y_data, model.thetas[threadnum][-1])
        model.thetas[threadnum].append(theta)
        iters = iters + 1
```

```python
def metropolishastings(model, y_data, blocksize, number_of_blocks):
    n = 0
    while n < number_of_blocks:
        for threadnum, thetas_onelot in enumerate(model.thetas):
            metropolis_singlethread(model, y_data, threadnum, blocksize)
        n = n+1
```

Let's see this work. First, let's see what the likelihood is when we fix the measured data and take a variety of different proposed $k$ parameters.

```python
ks = np.linspace(0,10,100)
likelihoods = []
for k in ks:
    likelihoods.append(likelihood_k({'k':k}, noisy_data))

plt.figure()
plt.plot(ks, likelihoods)
plt.xlabel('input parameter, k')
plt.ylabel('likelihood')
plt.show()
```

```python
# Run the metropolis hastings algorithm

thetas_k = [[{'k':5}], [{'k':3}], [{'k':1}]] # Three initial guesses for k
covariates_k = {'k':0.05} # Step size (SD of normal distribution for choosing next proposed theta)
model = OdeModel(thetas_k, covariates_k, uniform_prior, likelihood_k, onecomp)
metropolishastings(model, noisy_data, 10, 100)
```

```python
# This is how k looks (from all start-points) as the algorithm progresses

plt.figure()
for n in range(len(model.thetas)):
    ks_list= [theta['k'] for theta in model.thetas[n]]
    plt.plot(ks_list[:500]) # only first 500
plt.xlabel('iteration #')
plt.ylabel('k')
plt.show()
```

```python
# Here are the occurrences of all k estimates throughout the algorithm

all_ks = []
for n in range(len(model.thetas)):
    ks_list = [theta['k'] for theta in model.thetas[n]]
    all_ks.append(ks_list)

plt.figure()
plt.hist(all_ks, bins=100, stacked=True)
plt.xlabel('k')
plt.ylabel('occurrence')
plt.show()
```

## Using Pints
Now let's try the same thing, but using Pints!
Once again, it's much easier to type and quicker to run than our manual method above.

```python
import pints
```

```python
# Next, sampling.
log_likelihood = pints.GaussianKnownSigmaLogLikelihood(problem, sigma=0.05)
startpoints = [[1],[3],[5]]
mcmc = pints.MCMCController(log_likelihood, 3, startpoints, method=pints.HaarioBardenetACMC)
mcmc.set_max_iterations(2000)
mcmc.set_log_to_screen(False)
samples = mcmc.run()
```

```python
# Use a diagnostic plot to check if the chains have converged
import pints.plot
pints.plot.trace(samples)
plt.show()
```

```python
# Plot several predictions that are all likely sources of the experimental data
pints.plot.series(np.vstack(samples[:,1000:]), problem)
plt.show()
```

# Hierarchical inference

For our case, the probability is
$$\begin{eqnarray}
\mathcal{P} &=& P(y_j, \theta_j, \mu, \tau)
\\&=&  P(\mu) \cdot P(\tau) \cdot \prod_{j=1}^8 P(y_j|\theta_j,\sigma_j) \cdot P(\theta_j|\mu,\tau)
\\&=& N(\mu_\mu,\sigma_\mu) \cdot C(\mu_\tau, \sigma_\tau) \cdot \prod_{j=1}^8 N(\theta_j,\sigma_j) \cdot N(\mu,\tau)
\\&=& \frac{1}{\sqrt{2\pi\sigma_\mu^2}}e^{\frac{-(\mu-\mu_\mu)^2}{2\sigma_\mu^2}} \cdot \frac{\sigma_\tau^2}{(\tau-\mu_\tau)^2 + \sigma_\tau^2} \cdot \prod_{j=1}^8 \Bigg(\frac{1}{\sqrt{2\pi\sigma^2}}e^{\frac{-(y_j-\theta_j)^2}{2\sigma_j^2}} \cdot \frac{1}{\sqrt{2\pi\tau^2}}e^{\frac{-(\theta_j-\mu)^2}{2\tau^2}} \Bigg)
\end{eqnarray}$$

And so the log probability is
$$\begin{eqnarray}
\log\mathcal{P} = \mathcal{L} &=& \log\bigg(\sum_{j=1}^8 P(y_j|\theta_j,\sigma_j)P(\theta_j|\mu,\tau)P(\mu)P(\tau)\bigg)
\\&=& \sum_{j=1}^8 \log\bigg(\frac{1}{\sqrt{2\pi\sigma^2}}e^{\frac{-(y_j-\theta_j)^2}{2\sigma_j^2}}\bigg)
+ \sum_{j=1}^8 \log\bigg(\frac{1}{\sqrt{2\pi\tau^2}}e^{\frac{-(\theta_j-\mu)^2}{2\tau^2}}\bigg)
+ \log\bigg(\frac{1}{\sqrt{2\pi\sigma_\mu^2}}e^{\frac{-(\mu-\mu_\mu)^2}{2\sigma_\mu^2}}\bigg)
+ \log\bigg(\frac{1}{\sqrt{2\pi\sigma_\tau^2}}e^{\frac{-(\mu-\mu_\tau)^2}{2\sigma_\tau^2}}\bigg)
\end{eqnarray}$$

Note that the exponentials can be further separated from the fractions if required, using the log sum law: $\log(ab) = \log(a)+\log(b)$

```python
# Parameters
mu = 1.5
tau = 0.3
ks = [abs(n) for n in np.random.normal(mu, tau, 5)]
sigma = 0.1
manytimes = np.linspace(0,10,100)
times = np.linspace(0,10,11)

# A one-compartment PK model is basically an ODE for an exponential decay curve
def onecomp(y, t, k):
    """A one-compartment PK model, aka simple exponential decay
    In
    y: y-axis value
    t: x-axis value
    k: gradient parameter

    Out: gradient at the given point
    """
    dydt = -k * y
    return dydt

# You can solve ODEs with the Forward Euler method
def ForwardEuler(func, k, y0, times):
    """Numerically calculate an ODE (forward Euler technique)
    In
    func: function giving the derivative
    k: parameters the function requires as input
    y0: y at t=0
    times: array of times at which to calculate y

    Out: array of y values at corresponding points in times
    """
    y = y0
    y_solution = [y0]
    h = times[2] - times[1]

    for n in times[:-1]:
        dy = func(y, n, k)
        y = y + h*dy
        y_solution.append(y)

    return y_solution

def simulate(func, parameters, y0, times):
    """Numerically calculate an ODE
    In
    func: function giving the derivative
    parameters: parameters the function requires as input
    y0: y at t=0
    times: array of times at which to calculate y

    Out: array of y values at corresponding points in times
    """
    return scipy.integrate.odeint(func, y0, times, (parameters,))

# This is what the model looks like. We don't actually know these values
actual_values = [simulate(onecomp, k, 1, times) for k in ks]
manyvalues = [simulate(onecomp, k, 1, manytimes) for k in ks]
forwardeuler = [ForwardEuler(onecomp, k, 1, times) for k in ks]
manyeuler = [ForwardEuler(onecomp, k, 1, manytimes) for k in ks]

# Plot the model results
plt.figure()
plt.xlabel('Time')
plt.ylabel('Concentration')
for k in range(len(actual_values)):
    plt.plot(times, actual_values[k])
    plt.plot(manytimes, manyvalues[k])
plt.show()
```

```python
# Make noisy data that we're inferring from. noisy_data is known to us.
sampletimes = np.linspace(0,10,11)
noisy_data = []
noise_magnitude = sigma
fig, axes = plt.subplots(len(actual_values), figsize=(12,12), sharey=True)

for k in range(len(actual_values)):
    noise = np.random.normal(0, noise_magnitude, 11)
    noisy_data.append([a+b for a, b in zip(actual_values[k], noise)])
    axes[k].plot(sampletimes, noisy_data[k], 'x')
    axes[k].plot(times, actual_values[k])
    axes[k].set_xlim([0, 10])

plt.xlabel('Time')
plt.show()
```

So what's the likelihood of a given set of parameters? We have:-
* $y_{jt}$ is the output of our ODE model: the concentration of drug. It varies by time, t, and by person, j. It depends on $k_j$, and the measurements depend on the measurement error, $\sigma$. We know $\sigma$ in advance.
* $k_j$ is the parameter of our ODE model: the decay constant of the ODE. It is different for each person, j. For each person, it is simply a single scalar taken randomly from a normal distribution, $N(\mu, \tau)$.
* $\mu$ is the mean of all parameters, $k_j$. We do not know it, and we assume that it is drawn from a normal distribution with known mean $\mu_\mu$ and known variance $\sigma_\mu$. This is part of our prior.
* $\tau$ is the variance of all parameters about the mean. Like $\mu$ we do not know it, and its probability distribution forms part of our prior. We assume a half-Cauchy distribution with known mean $\mu_\tau$ and known variance $\sigma_\tau$.

The probability, $\mathcal{P}$, of a set of parameters being the correct ones for measured data, $y_{jt}$, is:-
$$\begin{eqnarray}
\mathcal{P} &=& P(y_{jt}, k_j, \mu, \tau)
\\&=& \prod_{j=1}^8 \prod_{t=0}^T P(y_{jt}|k_j,\sigma) \cdot P(k_j|\mu,\tau) \cdot P(\mu) \cdot P(\tau)
\end{eqnarray}$$

From this, the log probability is:-
$$\begin{eqnarray}
\mathcal{L} &=& \sum_{j=1}^8 \sum_{t=0}^T \log \bigg( \frac{1}{\sqrt{2\pi\sigma^2}}e^{\frac{-(y_{jt}-k_j)^2}{2\sigma^2}}\bigg) + \log \bigg( \frac{1}{\sqrt{2\pi\tau^2}}e^{\frac{-(k_j-\mu)^2}{2\tau^2}}\bigg) + \log \bigg( \frac{1}{\sqrt{2\pi\sigma_\mu^2}}e^{\frac{-(\mu-\mu_\mu)^2}{2\sigma_\mu^2}}\bigg) + \log \bigg( \frac{\sigma_\tau^2}{(\tau-\mu_\tau)^2 + \sigma_\tau^2} \bigg)
\end{eqnarray}$$

```python
def loglikelihood_onecomp(x, actual_data):
    mu = x[0]
    tau = x[1]
    ks = x[2:]

    # priors
    k_log_prior = pints.GaussianLogPrior(mu, tau)
    mu_log_prior = pints.HalfCauchyLogPrior(0,1)
    tau_log_prior = pints.HalfCauchyLogPrior(0,1)

    lk_of_mu = mu_log_prior([mu]) # log probability of mu
    lk_of_tau = tau_log_prior([tau]) # log probability of tau

    lk_of_data = 0
    lk_of_k = 0
    for j, proposed_k in enumerate(ks): # for each person j
        lk_of_k += k_log_prior([proposed_k]) # log probability of proposed k given mu & tau
        ys = simulate(onecomp, proposed_k, 1, times)
        for t, y in enumerate(ys): # for each time t
            data_log_prior = pints.GaussianLogPrior(y, sigma)
            lk_of_data += data_log_prior([actual_data[j][t]]) # log probability of data for j & t

    return lk_of_mu + lk_of_tau + lk_of_k + lk_of_data

class an_ODE_to_pints(pints.LogPDF):
    def __init__(self):
        self._n_parameters = 7
    def __call__(self, x, y_data=actual_values):
        return loglikelihood_onecomp(x, y_data)
    def n_parameters(self):
        return self._n_parameters
```

```python
model = an_ODE_to_pints()

# Propose start parameters, normal distribution but making all negative params +ve
num_threads = 4
start_params = [[abs(n) for n in np.random.normal(1, 1, 7)] for n in range(num_threads)]

mcmc = pints.MCMCController(model, num_threads, start_params, method=pints.HaarioBardenetACMC)
mcmc.set_log_to_screen(True)
mcmc.set_log_interval(100)

full_chains = mcmc.run()
```

    Using Haario-Bardenet adaptive covariance MCMC
    Generating 4 chains.
    Running in sequential mode.
    Iter. Eval. Accept.   Accept.   Accept.   Accept.   Time m:s
    0     4      0         0         0         0          0:00.0
    1     8      0         0         0.5       0          0:00.0
    2     12     0         0.333     0.667     0          0:00.0
    3     16     0.25      0.5       0.75      0          0:00.0
    100   404    0.495     0.515     0.574     0.535      0:00.9
    Initial phase completed.
    200   804    0.532     0.537     0.438     0.478      0:01.9
    300   1204   0.472     0.488     0.409     0.435      0:02.8
    400   1604   0.394015  0.399     0.354     0.372      0:03.7
    500   2004   0.351     0.369     0.323     0.329      0:04.7
    600   2404   0.324     0.334     0.296173  0.301      0:05.8
    700   2804   0.305     0.301     0.284     0.285      0:06.8
    800   3204   0.289638  0.295     0.275     0.265      0:07.8
    900   3604   0.281     0.281     0.261     0.254162   0:08.8
    1000  4004   0.272     0.264     0.26      0.251      0:09.7
    1100  4404   0.262     0.259     0.243     0.247      0:10.7
    1200  4804   0.249     0.256453  0.24      0.25       0:11.6
    1300  5204   0.235     0.255957  0.234435  0.244      0:12.6
    1400  5604   0.235546  0.251     0.231     0.241      0:13.5
    1500  6004   0.234     0.242505  0.236509  0.239      0:14.4
    1600  6404   0.224     0.233604  0.235     0.236      0:15.3
    1700  6804   0.221     0.233     0.235     0.233      0:16.3
    1800  7204   0.221     0.232     0.229     0.229317   0:17.3
    1900  7604   0.226     0.229     0.22      0.224      0:18.3
    2000  8004   0.22      0.232     0.218     0.224      0:19.2
    2100  8404   0.223703  0.229     0.225     0.227      0:20.2
    2200  8804   0.229     0.227     0.226     0.225      0:21.2
    2300  9204   0.233     0.225     0.227     0.225      0:22.1
    2400  9604   0.235     0.227     0.231     0.225      0:23.1
    2500  10004  0.232     0.226     0.224     0.22       0:24.0
    2600  10404  0.233     0.225     0.217     0.224      0:25.0
    2700  10804  0.235     0.224     0.22      0.225      0:26.0
    2800  11204  0.231     0.227     0.221     0.227      0:26.9
    2900  11604  0.231     0.229     0.219     0.225      0:27.9
    3000  12004  0.233922  0.226     0.223     0.224      0:28.9
    3100  12404  0.237     0.227346  0.232     0.223      0:29.8
    3200  12804  0.236     0.224     0.234     0.218      0:30.8
    3300  13204  0.231     0.22      0.235     0.216904   0:31.8
    3400  13604  0.227     0.216995  0.235519  0.216995   0:32.8
    3500  14004  0.232     0.219     0.233     0.217      0:33.7
    3600  14404  0.228     0.219     0.232     0.216      0:34.7
    3700  14804  0.227     0.22      0.227     0.222      0:35.6
    3800  15204  0.225467  0.217     0.223     0.217      0:36.7
    3900  15604  0.226     0.217     0.224     0.215      0:37.7
    4000  16004  0.227     0.218     0.223     0.217      0:38.7
    4100  16404  0.225     0.214338  0.224     0.213      0:39.7
    4200  16804  0.228517  0.213     0.224     0.214      0:40.7
    4300  17204  0.229     0.214     0.223     0.215      0:41.7
    4400  17604  0.229     0.211     0.223     0.215      0:42.6
    4500  18004  0.226     0.212     0.222     0.218      0:43.6
    4600  18404  0.223     0.211     0.223     0.219      0:44.6
    4700  18804  0.221     0.208679  0.224     0.219      0:45.6
    4800  19204  0.225     0.207     0.223     0.218      0:46.5
    4900  19604  0.224     0.207     0.222     0.218      0:47.5
    5000  20004  0.226     0.210158  0.225     0.216      0:48.5
    5100  20404  0.225446  0.215     0.227     0.216      0:49.5
    5200  20804  0.224     0.219     0.226     0.216      0:50.4
    5300  21204  0.226     0.217     0.225     0.217      0:51.4
    5400  21604  0.224     0.216     0.225     0.217      0:52.4
    5500  22004  0.225     0.214     0.224     0.216      0:53.4
    5600  22404  0.227     0.216     0.228     0.218      0:54.4
    5700  22804  0.229     0.214     0.228     0.22       0:55.4
    5800  23204  0.227     0.211     0.226     0.22       0:56.4
    5900  23604  0.226     0.215     0.225     0.219      0:57.4
    6000  24004  0.224     0.217     0.222     0.219      0:58.4
    6100  24404  0.224     0.219     0.222     0.22       0:59.4
    6200  24804  0.222     0.222     0.224     0.221      1:00.4
    6300  25204  0.222     0.222187  0.221     0.222      1:01.4
    6400  25604  0.224     0.224     0.22      0.222      1:02.3
    6500  26004  0.221     0.226     0.217505  0.221      1:03.3
    6600  26404  0.219     0.226     0.21421   0.22       1:04.3
    6700  26804  0.222     0.227     0.217     0.22       1:05.3
    6800  27204  0.221     0.226     0.217     0.219      1:06.3
    6900  27604  0.223     0.227     0.22      0.218664   1:07.3
    7000  28004  0.221     0.227     0.221     0.219      1:08.4
    7100  28404  0.219     0.225     0.222363  0.22       1:09.5
    7200  28804  0.22      0.223     0.221     0.221      1:10.4
    7300  29204  0.221     0.223     0.22      0.223      1:11.4
    7400  29604  0.221     0.221     0.221     0.224      1:12.4
    7500  30004  0.221     0.22      0.223     0.225      1:13.4
    7600  30404  0.224     0.219     0.223     0.223      1:14.4
    7700  30804  0.225     0.221     0.221     0.221      1:15.4
    7800  31204  0.224     0.222     0.224     0.222      1:16.4
    7900  31604  0.225     0.224     0.223     0.222      1:17.4
    8000  32004  0.223722  0.223     0.223     0.223      1:18.4
    8100  32404  0.222     0.221701  0.224     0.224      1:19.4
    8200  32804  0.222168  0.219     0.225     0.224      1:20.4
    8300  33204  0.222     0.222     0.226     0.225      1:21.4
    8400  33604  0.222     0.221     0.226     0.225      1:22.3
    8500  34004  0.222     0.22      0.226     0.227      1:23.4
    8600  34404  0.222     0.22      0.227     0.227      1:24.4
    8700  34804  0.22      0.221     0.225     0.226      1:25.4
    8800  35204  0.218     0.223     0.225     0.225      1:26.4
    8900  35604  0.219     0.224     0.224     0.225      1:27.4
    9000  36004  0.22042   0.222     0.226     0.225      1:28.4
    9100  36404  0.221     0.224     0.227     0.224      1:29.4
    9200  36804  0.221     0.223     0.227     0.226      1:30.3
    9300  37204  0.221     0.222     0.226     0.225      1:31.3
    9400  37604  0.223     0.224     0.224     0.225      1:32.3
    9500  38004  0.221     0.226     0.225871  0.225      1:33.3
    9600  38404  0.221     0.227     0.226     0.225      1:34.3
    9700  38804  0.220802  0.228     0.227     0.225      1:35.4
    9800  39204  0.222     0.22814   0.226     0.226      1:36.4
    9900  39604  0.222     0.227     0.225     0.224      1:37.3
    10000 40000  0.2228    0.2255    0.2248    0.2221     1:38.3
    Halting: Maximum number of iterations (10000) reached.

Let's see how the estimation fares. In the plot below, each row represents a parameter and the vertical black lines show the 'true' value of that parameter.

```python
fig, axs = pints.plot.trace(full_chains)
all_params = [mu]+[tau]+ks
param_names = ['mu','tau','k1','k2','k3','k4','k5']
for row, n in enumerate(all_params):
    axs[row][0].axvline(n, color='k')
    axs[row][0].set(xlabel=param_names[row])

plt.show()
```

.ipynb_checkpoints/Beginners' tutorial suppl mat-checkpoint.ipynb


# A soft module approach to modelling the MAPK cascade
In typical bond graph approaches to hierarchical modelling (and also in many computational models seen in engineering and programming), modules are specified through the use of hard ports which are unable to be changed after the development of a model. However, in systems biology, researchers are increasingly favouring the use of a "soft port" approach in which ports are flexible, and ports can be exposed during the process of coupling models together. This approach requires a shift away from the "black-box" modularity commonly seen in engineering, and requires the internal details of a model to be externally accessible. Examples of this approach are seen in the *little b* and PySB software packages among others, but the scope of these packages are often limited to kinetic models of biochemical systems (which are often thermodynamically inconsistent). Up until recently, an equivalent approach was not available through the use of bond graphs. However, the development of BondGraphTools has enabled a soft module approach to be taken.

In this notebook, a model of the MAPK cascade is constructed using the idea of soft modules to demonstrate the feasibility of this approach using bond graphs. It will be shown that this approach has the following implications:
- At each point in the hierarchy, individual models are able to be independently simulated.
- The code explicitly records the components that are merged together.
- Submodules are able to be assigned attributes that contain information about them. From a programming point of view, this can be useful in accessing information that summarises how modules are constructed. From a model development perspective, these attributes can also be used to store potentially important information such as biological annotations or the provenance of a model.
- A model's parameters can be inherited from its submodules. This means that parameters only need to be specified once, provided that none of the parameters conflict with each other. In the case where components with different parameters are merged, the modeller needs to decide how to resolve the conflict. However, the resulting parameter assignment is made explicit. A potential disadvantage of this approach is that the code required to describe parameter inheritance adds some overhead to the development of small models.
- Classes of models with similar (but not identical) structures can be automatically constructed.
- An implication of this approach not discussed here is that is is straightforward to make incremental changes to existing models. Furthermore, the changes are explicitly documented within the code, which may make recording model provenance easier.

```python
import BondGraphTools as bgt
from BondGraphTools import BondGraph
import numpy as np
from matplotlib import pyplot as plt

import sys
import bgt_extensions as bgex
from bgt_extensions import Ce, Se, unify, promote
from general import mpl_settings, save_figure
from plot_options import set_color_palette
set_color_palette()
```

## Kinase
Below, a generic model of a kinase is constructed. The model contains the reaction network
$$
\begin{align}
E + M + ATP \rightleftharpoons C \\
C \rightleftharpoons E + MP + ADP
\end{align}
$$
ATP and ADP are considered chemostats in this model.

Note that in this notebook, models are constructed using programming classes rather than functions. This is useful as it allows models to be assigned arbitrary attributes. In the example below, the class is used to describe the default parameters of the model (which can later be inherited by models that use it).

```python
affinity_ATP_hydrolysis = 50000/8.314/310

class Kinase(BondGraph):
    def __init__(self,name="Kinase",K_M=1.0,K_E=1.0):
        kinase_reactions = [
            ("E + M + ATP = C","r1"),
            ("C = E + MP + ADP","r2")
        ]
        chemostats = ["ATP","ADP"]
        self.K_M = K_M
        self.K_E = K_E

        BondGraph.__init__(self,name=name)
        bgex.add_reactions(self,kinase_reactions,chemostats,normalised=True)

    @property
    def default_params(self):
        self.ATP_potential = affinity_ATP_hydrolysis
        self.ADP_potential = 0.0
        ATP_potential = self.ATP_potential

        a = 1000
        d = 150
        k = 150

        D = (a*k/d)**2*np.exp(-affinity_ATP_hydrolysis)
        P_factor = (np.sqrt(D)/k)*(d/a)*np.exp(affinity_ATP_hydrolysis)

        K_C = np.exp(ATP_potential)*d/a*self.K_M*self.K_E
        K_MP = self.K_M*P_factor
        r1 = d/K_C
        r2 = k/K_C

        params = {
            ("C:E","k"): self.K_E,
            ("C:C","k"): K_C,
            ("C:M","k"): self.K_M,
            ("C:MP","k"): K_MP,
            ("SS:ATP","e"): ATP_potential,
            ("SS:ADP","e"): self.ADP_potential,
            ("R:r1","r"): r1,
            ("R:r2","r"): r2
        }

        return params

    def set_params(self, params=None):
        if not params:
            params = self.default_params
        bgex.set_parameters(self,params)
```

An instance of the kinase is built below. Note that the kinase has a set of differential equations, and can therefore be simulated independently of any external inputs.

```python
model = Kinase()
model.set_params()
bgt.draw(model)
model.constitutive_relations
```

The default parameters of the model can be accessed using the code below:

```python
model.default_params
```

    {('C:E', 'k'): 1.0,
     ('C:C', 'k'): 39933509.573638074,
     ('C:M', 'k'): 1.0,
     ('C:MP', 'k'): 16316.353672239,
     ('SS:ATP', 'e'): 19.39984635321688,
     ('SS:ADP', 'e'): 0.0,
     ('R:r1', 'r'): 3.7562438563882655e-06,
     ('R:r2', 'r'): 3.7562438563882655e-06}

## Phosphatase
A similar approach can be taken for the phosphatase, with the reaction scheme
$$
\begin{align}
E + MP \rightleftharpoons C \\
C \rightleftharpoons E + M + P
\end{align}
$$
where P is considered to be a chemostat.

```python
class Phosphatase(BondGraph):
    def __init__(self,name="Phosphatase",K_MP=1.0,K_E=1.0):
        phosphatase_reactions = [
            ("E + MP = C","r1"),
            ("C = E + M + P","r2")
        ]
        chemostats = ["P"]
        self.K_MP = K_MP
        self.K_E = K_E

        BondGraph.__init__(self,name=name)
        bgex.add_reactions(self,phosphatase_reactions,chemostats,normalised=True)

    @property
    def default_params(self):
        self.P_potential = 0.0

        a = 1000
        d = 150
        k = 150

        D = (a*k/d)**2*np.exp(-affinity_ATP_hydrolysis)
        P_factor = (np.sqrt(D)/k)*(d/a)*np.exp(affinity_ATP_hydrolysis)

        K_C = d/a*self.K_MP*self.K_E
        K_M = self.K_MP/P_factor
        r1 = d/K_C
        r2 = k/K_C

        params = {
            ("C:E","k"): self.K_E,
            ("C:C","k"): K_C,
            ("C:M","k"): K_M,
            ("C:MP","k"): self.K_MP,
            ("SS:P","e"): self.P_potential,
            ("R:r1","r"): r1,
            ("R:r2","r"): r2
        }
        return params

    def set_params(self,params=None):
        self.P_potential = -5.0
        if not params:
            params = self.default_params
        bgex.set_parameters(self,params)
```

```python
model = Phosphatase()
model.set_params()
bgt.draw(model)
model.constitutive_relations
```

## Phosphorylation cycle
The kinase and phosphatase models are combined to make a model of the phosphorylation cycle. The code below describes the set of actions required to construct a model of the phosphorylation cycle:
- The kinase and phosphatase are imported into a model of the phosphorylation cycle
- The unphosphorylated substrates M and phosphorylated MP are shared between both modules. Therefore they are "unified" in the model of the phosphorylation cycle, i.e. the bonds that originally connected those components are exposed, and linked to a single unified component through a zero junction that sums their rates of accumulation.
- Because the merging of M and MP combined two components into one, we need to decide how to assign parameters for the merged component. In this case, the parameters were identical in the submodules, so the parameter was taken from the kinase model in both cases.
- The parameters for the other components can be directly inherited from the kinase or phosphatase modules.

```python
class PhosphorylationCycle(BondGraph):
    def __init__(self,name="Phosphorylation Cycle",K_M=1.0,K_MP=1.0,K_Kin=1.0):
        k = Kinase(K_M=K_M,K_E=K_Kin)
        p = Phosphatase(K_MP=K_MP)
        BondGraph.__init__(self,name=name,components=(k,p))

        self.kinase = k
        self.phosphatase = p
        self.K_M = K_M
        self.K_MP = K_MP
        self.K_Kin = K_Kin

        M = unify((k/"C:M",p/"C:M"),"M")[0]
        MP = unify((k/"C:MP",p/"C:MP"),"MP")[0]

        bgex.set_aqueous_solution_parameters(self,R=1.0,T=1.0)

    @property
    def default_params(self):
        params = {
            ("C:M","k"): self.kinase.default_params[("C:M","k")],
            ("C:MP","k"): self.kinase.default_params[("C:MP","k")],
        }

        return params

    def set_params(self, params=None):
        (self.kinase).set_params()
        (self.phosphatase).set_params()

        if not params:
            params = self.default_params
        bgex.set_parameters(self,params)
```

```python
model = PhosphorylationCycle()
model.set_params()
bgt.draw(model)
model.constitutive_relations
```

## Multisite phosphorylation
Individual "levels" of the MAPK cascade can be interpreted in terms of the number of sites of phosphorylation. For example in the model used in Huang and Ferrell (1996; doi: 10.1073/pnas.93.19.10078), the activation of MAPKKK requires only a single phosphorylation event. However, both MAPKK and MAPK contain two phosphorylation sites that need to be sequentially phosphorylated for those substrates to become active, and thus the activation of those substrates can be interpreted as dual phosphorylation cycles. More generally, substrates can have an arbitrary number of phosphorylation sites (and increasing the number of phosphorylation sites has been thought to increase the ultrasensitivity of the cascade), thus it is useful to have a general constructor for phosphorylation chains of arbitrary length. The code below implements such a constructor.

```python
a = 1000
d = 150
k = 150
D = (a*k/d)**2*np.exp(-affinity_ATP_hydrolysis)
P_factor_default = (np.sqrt(D)/k)*(d/a)*np.exp(affinity_ATP_hydrolysis)

class MultisitePhosphorylation(BondGraph):
    def __init__(self,n,name=None,K_M=1.0,P_factor=P_factor_default,K_Kin=1.0):
        self.n = n
        if not name:
            name=f"{n}-site phosphorylation"

        K_array = [K_M*P_factor**i for i in range(n+1)]
        cycles = tuple(
            PhosphorylationCycle(name=f"Step {i+1}",
                                 K_M=K_array[i],K_MP=K_array[i+1],K_Kin=K_Kin)
            for i in range(n)
        )
        self.cycles = cycles
        BondGraph.__init__(self,name=name,components=cycles)

        # Merge substrates
        promote(cycles[0]/"C:M")
        end_species = cycles[-1]/"C:MP"
        promote(end_species)
        end_species.name = "M"+"P"*n

        for i in range(n-1):
            unify((cycles[i]/"C:MP",cycles[i+1]/"C:M"),"M"+"P"*(i+1))

        ATP_components = tuple(cycle.kinase/"SS:ATP" for cycle in cycles)
        ADP_components = tuple(cycle.kinase/"SS:ADP" for cycle in cycles)
        P_components = tuple(cycle.phosphatase/"SS:P" for cycle in cycles)
        equivalent_components = [
            (ATP_components, "ATP"),
            (ADP_components, "ADP"),
            (P_components, "P"),
        ]
        for components,species in equivalent_components:
            for comp in components:
                promote(comp)
            unify(components,species)

        # Merge kinases
        kinase_enzymes = tuple(c/"Kinase"/"C:E" for c in cycles)
        for i,s in enumerate(kinase_enzymes):
            promote(s)
            s.name = "Kin"
        unify(kinase_enzymes,"Kin")

        # Merge phosphatases
        phosphatase_enzymes = tuple(c/"Phosphatase"/"C:E" for c in cycles)
        for s in phosphatase_enzymes:
            promote(s)
            s.name = "Pho"
        unify(phosphatase_enzymes,"Pho")

        bgex.set_aqueous_solution_parameters(self,R=1.0,T=1.0)

    @property
    def default_params(self):
        cycles = self.cycles
        cycle1 = cycles[0]
        kinase1 = cycle1.kinase
        phosphatase1 = cycle1.phosphatase
        params = {
            ("C:Kin","k"): kinase1.default_params[("C:E","k")],
            ("C:Pho","k"): phosphatase1.default_params[("C:E","k")],
            ("SS:ATP","e"): kinase1.default_params[("SS:ATP","e")],
            ("SS:ADP","e"): kinase1.default_params[("SS:ADP","e")],
            ("SS:P","e"): phosphatase1.default_params[("SS:P","e")],
        }

        for i in range(self.n):
            phosphates = "P"*i
            params[(f"C:M{phosphates}","k")] = cycles[i].default_params[("C:M","k")]

        phosphates = "P"*(self.n)
        params[(f"C:M{phosphates}","k")] = cycles[-1].default_params[("C:MP","k")]
        return params

    def set_params(self, params=None):
        for c in self.cycles:
            c.set_params()

        if not params:
            params = self.default_params
        bgex.set_parameters(self,params)
```

A phosphorylation chain of length 1 is equivalent to the phosphorylation cycle seen in the above section. However, for convenience, many components (M, MP, ATP, ADP, P, kinase enzyme, phosphatase enzyme) have been exposed a level up in the model hierarchy.

```python
model = MultisitePhosphorylation(n=1)
model.set_params()
bgt.draw(model)
model.constitutive_relations
```

The code is easily modified to create a phosphorylation chain of length 2. Note that in this case, the following components need to be merged:
- ATP, ADP and P need to be shared between cycles
- The species with a single phosphorylated site needs to be shared between phosphorylation cycles
- It is generally assumed that the same kinase/phosphatase is responsible for all phosphorylation/dephosphorylation events in the chain. Therefore these need to be merged as well.

```python
model = MultisitePhosphorylation(n=2)
model.set_params()
bgt.draw(model)
model.constitutive_relations
```

```python
model.default_params
```

    {('C:Kin', 'k'): 1.0,
     ('C:Pho', 'k'): 1.0,
     ('SS:ATP', 'e'): 19.39984635321688,
     ('SS:ADP', 'e'): 0.0,
     ('SS:P', 'e'): 0.0,
     ('C:M', 'k'): 1.0,
     ('C:MP', 'k'): 16316.353672239,
     ('C:MPP', 'k'): 266223397.1575871}

## A model of the MAPK cascade
Models of multi-site phosphorylation can then be combined into a model of the MAPK cascade. The model can be constructed by first making models of individual "levels" within the cascade and then linking them together. The first level (corresponding to MAPKKK) contains a single phosphorylation site, whereas the two later levels are described by dual phosphorylation chains. The models are coupled together by merging the following components:
- MAPKKKP links the first and second level, and MAPKKPP links the second and third levels, thus they are merged together
- The components corresponding to ATP, ADP and P are merged

For convenience, the components corresponding to the upstream molecule MAPKKKK and output molecule MAPKPP are elevated to the highest level of the model hierarchy.

```python
class MAPK(BondGraph):
    def __init__(self,name="MAPK"):
        level1 = MultisitePhosphorylation(n=1,name="Level 1",K_Kin=1.0)
        level2 = MultisitePhosphorylation(
            n=2,name="Level 2",K_Kin=level1.default_params[('C:MP','k')]
        )
        level3 = MultisitePhosphorylation(
            n=2,name="Level 3",K_Kin=level2.default_params[('C:MPP','k')]
        )

        levels = (level1,level2,level3)
        self.levels = levels

        BondGraph.__init__(self,name=name,components=(level1,level2,level3))

        # Promote input and output molecules
        MKKKK = level1/"C:Kin"
        promote(MKKKK)
        MKKKK.name = "MKKKK"

        MKPP = level3/"C:MPP"
        promote(MKPP)
        MKPP.name = "MKPP"

        # Merge phosphorylated substrates with kinases on next level of cascade
        MKKKP_components = (level1/"C:MP",level2/"C:Kin")
        unify(MKKKP_components,"MKKKP")

        MKKPP_components = (level2/"C:MPP",level3/"C:Kin")
        unify(MKKPP_components,"MKKPP")

        # Merge ATP, ADP and P
        energy_sources = ["ATP","ADP","P"]
        for s in energy_sources:
            components = tuple(l/f"SS:{s}" for l in levels)
            unify(components,s)

        bgex.set_aqueous_solution_parameters(self,R=1.0,T=1.0)

    @property
    def default_params(self):
        level1 = self.levels[0]
        level2 = self.levels[1]
        level3 = self.levels[2]
        params = {
            ("C:MKKKK","k"): level1.default_params[("C:M","k")],
            ("C:MKKKP","k"): level1.default_params[("C:MP","k")],
            ("C:MKKPP","k"): level2.default_params[("C:MPP","k")],
            ("C:MKPP","k"): level3.default_params[("C:MPP","k")],
            ("SS:ATP","e"): level1.default_params[("SS:ATP","e")],
            ("SS:ADP","e"): level1.default_params[("SS:ADP","e")],
            ("SS:P","e"): level1.default_params[("SS:P","e")],
        }
        return params

    def set_params(self, params=None):
        for l in self.levels:
            l.set_params()

        if not params:
            params = self.default_params
        bgex.set_parameters(self,params)
```

An instance of the MAPK model can then be created. It has a valid set of differential equations that can be simulated.

```python
model = MAPK()
model.set_params()
bgt.draw(model)
model.constitutive_relations
```

```python
from bgt_juliasim import simulate

initial_conditions = {
    model/"C:MKKKK": 3e-5, #Ras
    model/"Level 1"/"C:M": 0.003, #MKKK
    model/"Level 2"/"C:M": 1.2, #MKK
    model/"Level 3"/"C:M": 1.2, #MK
    model/"Level 1"/"C:Pho": 0.0003, #MAPKKK-Pase
    model/"Level 2"/"C:Pho": 0.0003, #MAPKK-Pase
    model/"Level 3"/"C:Pho": 0.12 #MAPK-Pase
}

def make_initial_conditions(model,initial_conditions):
    n_states = len(model.state_vars)
    x0 = np.zeros(n_states)
    for i,state_ref in enumerate(model.state_vars.values()):
        component = bgex.map_state_to_component(*state_ref)
        if component in initial_conditions.keys():
            x0[i] = initial_conditions[component]

    return x0

sol = simulate(model,(0.0,100.0),make_initial_conditions(model,initial_conditions))
```

```python
from bgt_extensions import plot_solution
mpl_settings()

def plot_kinases(sol,model):
    plt.figure()
    plot_solution(sol,model,model/"C:MKKKK")
    plt.legend(["MKKKK"])

    plt.figure()
    plot_solution(sol,model,model/"Level 1"/"C:M")
    plot_solution(sol,model,model/"C:MKKKP")
    plt.legend(["MKKK","MKKKP"])

    plt.figure()
    plot_solution(sol,model,model/"Level 2"/"C:M")
    plot_solution(sol,model,model/"Level 2"/"C:MP")
    plot_solution(sol,model,model/"C:MKKPP")
    plt.legend(["MKK","MKKP","MKKPP"])

    plt.figure()
    plot_solution(sol,model,model/"Level 3"/"C:M")
    plot_solution(sol,model,model/"Level 3"/"C:MP")
    plot_solution(sol,model,model/"C:MKPP")
    plt.legend(["MK","MKP","MKPP"])

plot_kinases(sol,model)
```

## Adding feedback
### Positive feedback
A module for positive feedback is created by using an existing module for phosphorylation at a single site, and making minor adjustments to the parameters. This is then composed with the MAPK model through the following operations:
- The MAPKPP species is unified with the kinase of the feedback module
- The MAPKKKK species is unified with the phosphorylated substrate (MP) of the feedback module. This represents the fact that the output catalyses the reaction that increases the pool of available input.
- ATP, ADP and P are unified

```python
class PositiveFeedback(MultisitePhosphorylation):
    def __init__(self,name="Feedback",K_M=1.0,P_factor=P_factor_default,K_Kin=1.0):
        MultisitePhosphorylation.__init__(
            self,n=1,name=name,K_M=K_M,P_factor=P_factor,K_Kin=K_Kin
        )

    @property
    def default_params(self):
        params = super().default_params

        phosphatase = self.cycles[0].phosphatase
        params[("Step 1/Phosphatase/R:r1","r")] = 10*phosphatase.default_params[("R:r1","r")]
        params[("Step 1/Phosphatase/R:r2","r")] = 10*phosphatase.default_params[("R:r2","r")]

        return params

class MAPK_PositiveFeedback(BondGraph):
    def __init__(self,name="MAPK with positive feedback"):
        mapk = MAPK()

        K_MKKKK = mapk.default_params[("C:MKKKK","k")]
        K_MKKKKI = K_MKKKK/P_factor_default
        self.K_MKKKKI = K_MKKKKI
        K_MKPP = mapk.default_params[("C:MKPP","k")]

        feedback = PositiveFeedback(
            name="Feedback",K_M=K_MKKKKI, K_Kin=K_MKPP
        )

        self.mapk = mapk
        self.feedback = feedback

        BondGraph.__init__(self,name=name,components=(mapk,feedback))

        unify((mapk/"C:MKKKK",feedback/"C:MP"),"MKKKK")
        unify((mapk/"C:MKPP",feedback/"C:Kin"),"MKPP")
        unify((mapk/"SS:ATP",feedback/"SS:ATP"),"ATP")
        unify((mapk/"SS:ADP",feedback/"SS:ADP"),"ADP")
        unify((mapk/"SS:P",feedback/"SS:P"),"P")

        bgex.set_aqueous_solution_parameters(self,R=1.0,T=1.0)

    @property
    def default_params(self):
        mapk = self.mapk

        params = {
            ("C:MKKKK","k"): mapk.default_params[("C:MKKKK","k")],
            ("C:MKPP","k"): mapk.default_params[("C:MKPP","k")],
            ("SS:ATP","e"): mapk.default_params[("SS:ATP","e")],
            ("SS:ADP","e"): mapk.default_params[("SS:ADP","e")],
            ("SS:P","e"): mapk.default_params[("SS:P","e")],
        }

        return params

    def set_params(self, params=None):
        self.mapk.set_params()
        self.feedback.set_params()

        if not params:
            params = self.default_params
        bgex.set_parameters(self,params)
```

```python
model = MAPK_PositiveFeedback()
model.set_params()
model.constitutive_relations
```

    [dx_0 + 300*x_0 - 306441016200029*x_18*x_24/5000000000000000 - 1000*x_2*x_24,
     dx_1 + 300*x_1 - 1000*x_18*x_3 - 612882032400059*x_2*x_3/10000000000000000,
     dx_2 - 150*x_0 - 150*x_1 + 1000*x_2*x_24 + 612882032400059*x_2*x_3/10000000000000000,
     dx_3 - 300*x_1 + 1000*x_18*x_3 + 612882032400059*x_2*x_3/10000000000000000,
     dx_4 - 1000*x_18*x_8 - 306441016200029*x_18*x_9/5000000000000000 + 300*x_4,
     dx_5 - 612882032400059*x_10*x_8/10000000000000000 - 1000*x_10*x_9 + 300*x_5,
     dx_6 - 612882032400059*x_18*x_19/10000000000000000 - 1000*x_18*x_9 + 300*x_6,
     dx_7 - 1000*x_10*x_19 - 612882032400059*x_10*x_9/10000000000000000 + 300*x_7,
     dx_8 + 612882032400059*x_10*x_8/10000000000000000 + 1000*x_18*x_8 - 150*x_4 - 150*x_5,
     dx_9 + 25001532205081*x_10*x_9/25000000000 + 25001532205081*x_18*x_9/25000000000 - 150*x_4 - 150*x_5 - 150*x_6 - 150*x_7,
     dx_10 + 1000*x_10*x_19 + 612882032400059*x_10*x_8/10000000000000000 + 10000612882032400059*x_10*x_9/10000000000000000 - 300*x_5 - 300*x_7,
     dx_11 + 300*x_11 - 1000*x_15*x_19 - 30644101620003*x_16*x_19/500000000000000,
     dx_12 + 300*x_12 - 612882032400059*x_15*x_17/10000000000000000 - 1000*x_16*x_17,
     dx_13 + 300*x_13 - 1000*x_16*x_19 - 306441016200029*x_19*x_25/5000000000000000,
     dx_14 + 300*x_14 - 612882032400059*x_16*x_17/10000000000000000 - 1000*x_17*x_25,
     dx_15 - 150*x_11 - 150*x_12 + 612882032400059*x_15*x_17/10000000000000000 + 1000*x_15*x_19,
     dx_16 - 150*x_11 - 150*x_12 - 150*x_13 - 150*x_14 + 25001532205081*x_16*x_17/25000000000 + 25001532205081*x_16*x_19/25000000000,
     dx_17 - 300*x_12 - 300*x_14 + 612882032400059*x_15*x_17/10000000000000000 + 25001532205081*x_16*x_17/25000000000 + 1000*x_17*x_25,
     dx_18 - 150*x_0 - 150*x_1 + 612882032400059*x_18*x_19/10000000000000000 + 306441016200029*x_18*x_24/5000000000000000 + 1000*x_18*x_3 + 1000*x_18*x_8 + 25001532205081*x_18*x_9/25000000000 - 300*x_4 - 300*x_6,
     dx_19 + 1000*x_10*x_19 - 300*x_11 - 300*x_13 + 1000*x_15*x_19 + 25001532205081*x_16*x_19/25000000000 + 612882032400059*x_18*x_19/10000000000000000 + 306441016200029*x_19*x_25/5000000000000000 - 150*x_6 - 150*x_7,
     dx_20 + 300*x_20 - 1000*x_22*x_25 - 306441016200029*x_24*x_25/5000000000000000,
     dx_21 + 3000*x_21 - 612882032400059*x_22*x_23/1000000000000000 - 10000*x_23*x_24,
     dx_22 - 150*x_20 - 1500*x_21 + 612882032400059*x_22*x_23/1000000000000000 + 1000*x_22*x_25,
     dx_23 - 3000*x_21 + 612882032400059*x_22*x_23/1000000000000000 + 10000*x_23*x_24,
     dx_24 - 300*x_0 + 306441016200029*x_18*x_24/5000000000000000 + 1000*x_2*x_24 - 150*x_20 - 1500*x_21 + 10000*x_23*x_24 + 306441016200029*x_24*x_25/5000000000000000,
     dx_25 - 150*x_13 - 150*x_14 + 1000*x_17*x_25 + 306441016200029*x_19*x_25/5000000000000000 - 300*x_20 + 1000*x_22*x_25 + 306441016200029*x_24*x_25/5000000000000000]

### Negative feedback
A negative feedback loop can also be represented as a phosphorylation event at a single site. This can be composed with the model of MAPK in a similar manner. However, in the case of negative feedback, MAPKKKK is unified with the unphosphorylated substrate (M) of the feedback module so that the output reduces the amount of active input.

```python
class NegativeFeedback(MultisitePhosphorylation):
    def __init__(self,name="Feedback",K_M=1.0,P_factor=P_factor_default,K_Kin=1.0):
        MultisitePhosphorylation.__init__(
            self,n=1,name=name,K_M=K_M,P_factor=P_factor,K_Kin=K_Kin
        )

    @property
    def default_params(self):
        params = super().default_params

        feedback_strength = 1e-4
        phosphatase = self.cycles[0].phosphatase
        kinase = self.cycles[0].kinase

        params = super().default_params
        params[("Step 1/Phosphatase/R:r1","r")] = 1e6*feedback_strength*phosphatase.default_params[("R:r1","r")]
        params[("Step 1/Phosphatase/R:r2","r")] = 1e6*feedback_strength*phosphatase.default_params[("R:r2","r")]
        params[("Step 1/Kinase/R:r1","r")] = feedback_strength*kinase.default_params[("R:r1","r")]
        params[("Step 1/Kinase/R:r2","r")] = feedback_strength*kinase.default_params[("R:r2","r")]

        return params

class MAPK_NegativeFeedback(BondGraph):
    def __init__(self,name="MAPK with negative feedback"):
        mapk = MAPK()

        K_MKKKK = mapk.default_params[("C:MKKKK","k")]
        K_MKPP = mapk.default_params[("C:MKPP","k")]

        feedback = NegativeFeedback(
            name="Feedback",K_M=K_MKKKK, K_Kin=K_MKPP
        )

        self.mapk = mapk
        self.feedback = feedback

        BondGraph.__init__(self,name=name,components=(mapk,feedback))

        unify((mapk/"C:MKKKK",feedback/"C:M"),"MKKKK")
        unify((mapk/"C:MKPP",feedback/"C:Kin"),"MKPP")
        unify((mapk/"SS:ATP",feedback/"SS:ATP"),"ATP")
        unify((mapk/"SS:ADP",feedback/"SS:ADP"),"ADP")
        unify((mapk/"SS:P",feedback/"SS:P"),"P")

        bgex.set_aqueous_solution_parameters(self,R=1.0,T=1.0)

    @property
    def default_params(self):
        mapk = self.mapk

        params = {
            ("C:MKKKK","k"): mapk.default_params[("C:MKKKK","k")],
            ("C:MKPP","k"): mapk.default_params[("C:MKPP","k")],
            ("SS:ATP","e"): mapk.default_params[("SS:ATP","e")],
            ("SS:ADP","e"): mapk.default_params[("SS:ADP","e")],
            ("SS:P","e"): mapk.default_params[("SS:P","e")],
        }

        return params

    def set_params(self, params=None):
        self.mapk.set_params()
        self.feedback.set_params()

        if not params:
            params = self.default_params
        bgex.set_parameters(self,params)
```

```python
model = MAPK_NegativeFeedback()
model.set_params()
model.constitutive_relations
```

    [dx_0 + 300*x_0 - 306441016200029*x_18*x_24/5000000000000000 - 1000*x_2*x_24,
     dx_1 + 300*x_1 - 1000*x_18*x_3 - 612882032400059*x_2*x_3/10000000000000000,
     dx_2 - 150*x_0 - 150*x_1 + 1000*x_2*x_24 + 612882032400059*x_2*x_3/10000000000000000,
     dx_3 - 300*x_1 + 1000*x_18*x_3 + 612882032400059*x_2*x_3/10000000000000000,
     dx_4 - 1000*x_18*x_8 - 306441016200029*x_18*x_9/5000000000000000 + 300*x_4,
     dx_5 - 612882032400059*x_10*x_8/10000000000000000 - 1000*x_10*x_9 + 300*x_5,
     dx_6 - 612882032400059*x_18*x_19/10000000000000000 - 1000*x_18*x_9 + 300*x_6,
     dx_7 - 1000*x_10*x_19 - 612882032400059*x_10*x_9/10000000000000000 + 300*x_7,
     dx_8 + 612882032400059*x_10*x_8/10000000000000000 + 1000*x_18*x_8 - 150*x_4 - 150*x_5,
     dx_9 + 25001532205081*x_10*x_9/25000000000 + 25001532205081*x_18*x_9/25000000000 - 150*x_4 - 150*x_5 - 150*x_6 - 150*x_7,
     dx_10 + 1000*x_10*x_19 + 612882032400059*x_10*x_8/10000000000000000 + 10000612882032400059*x_10*x_9/10000000000000000 - 300*x_5 - 300*x_7,
     dx_11 + 300*x_11 - 1000*x_15*x_19 - 30644101620003*x_16*x_19/500000000000000,
     dx_12 + 300*x_12 - 612882032400059*x_15*x_17/10000000000000000 - 1000*x_16*x_17,
     dx_13 + 300*x_13 - 1000*x_16*x_19 - 306441016200029*x_19*x_25/5000000000000000,
     dx_14 + 300*x_14 - 612882032400059*x_16*x_17/10000000000000000 - 1000*x_17*x_25,
     dx_15 - 150*x_11 - 150*x_12 + 612882032400059*x_15*x_17/10000000000000000 + 1000*x_15*x_19,
     dx_16 - 150*x_11 - 150*x_12 - 150*x_13 - 150*x_14 + 25001532205081*x_16*x_17/25000000000 + 25001532205081*x_16*x_19/25000000000,
     dx_17 - 300*x_12 - 300*x_14 + 612882032400059*x_15*x_17/10000000000000000 + 25001532205081*x_16*x_17/25000000000 + 1000*x_17*x_25,
     dx_18 - 150*x_0 - 150*x_1 + 612882032400059*x_18*x_19/10000000000000000 + 306441016200029*x_18*x_24/5000000000000000 + 1000*x_18*x_3 + 1000*x_18*x_8 + 25001532205081*x_18*x_9/25000000000 - 300*x_4 - 300*x_6,
     dx_19 + 1000*x_10*x_19 - 300*x_11 - 300*x_13 + 1000*x_15*x_19 + 25001532205081*x_16*x_19/25000000000 + 612882032400059*x_18*x_19/10000000000000000 + 306441016200029*x_19*x_25/5000000000000000 - 150*x_6 - 150*x_7,
     dx_20 + 3*x_20/100 - 30644101620003*x_22*x_25/5000000000000000000 - x_24*x_25/10,
     dx_21 + 30000*x_21 - 100000*x_22*x_23 - 612882032400059*x_23*x_24/100000000000000,
     dx_22 - 3*x_20/200 - 15000*x_21 + 100000*x_22*x_23 + 30644101620003*x_22*x_25/5000000000000000000,
     dx_23 - 30000*x_21 + 100000*x_22*x_23 + 612882032400059*x_23*x_24/100000000000000,
     dx_24 - 300*x_0 + 306441016200029*x_18*x_24/5000000000000000 + 1000*x_2*x_24 - 3*x_20/200 - 15000*x_21 + 612882032400059*x_23*x_24/100000000000000 + x_24*x_25/10,
     dx_25 - 150*x_13 - 150*x_14 + 1000*x_17*x_25 + 306441016200029*x_19*x_25/5000000000000000 - 3*x_20/100 + 30644101620003*x_22*x_25/5000000000000000000 + x_24*x_25/10]


# Numerical Experiments

With this notebook, you can experiment with some Runge-Kutta methods as described in the paper.

Run the cell below to activate the project environment containing the necessary packages.

```julia
import Pkg
Pkg.activate(".")
Pkg.instantiate()
```

# Setup

Run this cell once to setup some basic functions.

```julia
using StaticArrays
using OrdinaryDiffEq, DiffEqCallbacks
using SummationByPartsOperators
using LinearAlgebra, Printf
using Quadmath

using LaTeXStrings
import PyPlot; plt = PyPlot
using PyCall

# line cycler adapted to colourblind people
cycler = pyimport("cycler")
cycler.cycler
colours = ["#E69F00", "#56B4E9", "#009E73", "#0072B2", "#D55E00", "#CC79A7", "#F0E442"]
linestyles = ["-", "--", "-.", ":", "-", "--", "-."]
#markers = [">", "^", "<", "v", "+", "x", "."]
markers = ["4", "2", "3", "1", "+", "x", "."]
colourblind_cycler = (cycler.cycler(color=colours) + cycler.cycler(linestyle=linestyles))
plt.rc("axes", prop_cycle=colourblind_cycler)
plt.rc("lines", linewidth=2, markersize=12, markeredgewidth=1.5)

plt.rc("text", usetex=true)
plt.rc("text.latex", preamble="\\usepackage{newpxtext}\\usepackage{newpxmath}\\usepackage{commath}\\usepackage{mathtools}")
plt.rc("font", family="serif", size=18.)
plt.rc("savefig", dpi=200)
plt.rc("legend", fontsize="medium", fancybox=true, framealpha=0.5)

isdir("../figures") || mkdir("../figures")

# the explicit midpoint method
A = [
    0 0;
    1//2 0
]
b = [0, 1//1]
c = A * (zero(b) .+ 1)
order = 2
midpoint_rk2 = ExplicitRK(tableau=DiffEqBase.ExplicitRKTableau(A, c, b, order))

# a strongly stable second order method
A = [
    0//1 0 0 0;
    1 0 0 0;
    1 -1 0 0;
    -1 1 1 0
]
b = [1//4, 1//4, 1//4, 1//4]
c = A * (zero(b) .+ 1)
order = 2
stable_rk2 = ExplicitRK(tableau=DiffEqBase.ExplicitRKTableau(A, c, b, order))

# a strongly stable third order method
A = [
    0 0 0 0 0;
    1//2 0 0 0 0;
    1 0 0 0 0;
    1 0 -1 0 0;
    -3 2 1 1 0
]
b = [0, 2//3, 0, 1//6, 1//6]
c = A * (zero(b) .+ 1)
order = 3
stable_rk3 = ExplicitRK(tableau=DiffEqBase.ExplicitRKTableau(A, c, b, order))
```

# Time-Dependent Linear Operators

Here, the linear wave equation

\begin{equation}
\begin{aligned}
  \partial_t u(t,x) + \sin(t^2) \partial_x u(t,x) &= 0,
  && t \in (0,100), x \in [-1, 1],
  \\
  u(0,x) &= \sin(\pi x),
  && x \in [-1,1],
\end{aligned}
\end{equation}

with time-dependent coefficients and periodic boundary conditions is solved numerically.

```julia
function compute_advection_sint2(alg, dt, u0func)
    T = typeof(dt)

    D = periodic_derivative_operator(1, 2, -one(T), one(T), 50)
    x = SummationByPartsOperators.grid(D)
    u0 = u0func.(x)
    f = (du,u,D,t) -> mul!(du, D, u, -sin(t^2))
    tspan = (zero(T), 100*one(T))
    ode = ODEProblem(f, u0, tspan, D)

    saveat = range(first(tspan), stop=last(tspan), length=1000)
    saving = SavingCallback((u,t,int)->integrate(u->u^2, u, D), SavedValues(T,T), saveat=saveat)
    sol = solve(ode, alg, dt=dt, callback=saving, save_everystep=false, adaptive=false)
    t = saving.affect!.saved_values.t
    energy = saving.affect!.saved_values.saveval

    t, energy, opnorm(Matrix(D))
end
```

```julia
plt.close("all")

t, energy, opnormD = compute_advection_sint2(SSPRK33(), 1.e-5, sinpi)
@printf("The operator norm of D with respect to the standard inner product is %.3e.\n", opnormD)
plt.plot(t, energy, label="SSPRK33")

t, energy, opnormD = compute_advection_sint2(stable_rk2, 1.e-5, sinpi)
plt.plot(t, energy, label="Stable RK42")

plt.xlabel(L"t")
plt.ylabel(L"|\!| u(t) |\!|^2")
plt.legend()
plt.xlim(extrema(t))
plt.tight_layout()

plt.savefig("../figures/linear_time_dep_advection.pdf", bbox_inches="tight")
```

# A Special Nonlinear Autonomous System

The explicit midpoint method

\begin{align}
    y_2 & = u^n + \frac{h}{2}f(u^n), \\
    u^{n+1} & = u^n + h f(y_2)
\end{align}

conserves the energy for the ODE

\begin{align}
u_1'(t) & = \frac{-u_2}{u_1^2 + u_2^2}, \\
u_2'(t) & = \frac{ u_1}{u_1^2 + u_2^2}.
\end{align}

and all step sizes.

```julia
function compute_special_problem(alg, dt)
  T = typeof(dt)

  u0 = SVector(one(T), zero(T))
  f = (u,p,t) -> SVector(-u[2], u[1]) / sum(abs2, u)
  tspan = (zero(T), 20*one(T))
  ode = ODEProblem(f, u0, tspan)
  saving = SavingCallback((u,t,int)->sum(abs2, u), SavedValues(T,T))
  sol = solve(ode, alg, dt=dt, callback=saving, save_everystep=false, adaptive=false)

  t = saving.affect!.saved_values.t
  energy = saving.affect!.saved_values.saveval

  t, energy
end

function create_plots(dt; yaxis_log=false, save_legend=false)
    algs = [SSPRK33(), midpoint_rk2, stable_rk2, stable_rk3]
    labels = ["SSPRK33", "Midpoint", "Stable RK42", "Stable RK53"]

    plt.close("all")

    for (alg, label) in zip(algs, labels)
        t, energy = compute_special_problem(alg, dt)

        plt.plot(t, energy .- first(energy), label=label)
    end

    plt.xlabel(L"t")
    plt.ylabel(L"|\!| u(t) |\!|^2 - |\!| u_0 |\!|^2")
    plt.xlim((0, 20))
    plt.gca().ticklabel_format(axis="y", scilimits=(0,0))
    plt.tight_layout()
    yaxis_log && plt.yscale("symlog", linthreshy=1.e-10)

    plt.savefig("../figures/special_problem__dt_$(-round(Int, Float64(log10(dt)))).pdf", bbox_inches="tight")

    if save_legend
        ax = plt.gca()
        plt.figure()
        handles, labels = ax.get_legend_handles_labels()
        plt.figlegend(handles, labels, loc="center", ncol=4)
        plt.savefig("../figures/special_problem__legend.pdf", bbox_inches="tight")
    end

    nothing
end
```

```julia
T = Float128
create_plots(T(1.e-1), yaxis_log=false, save_legend=true)
create_plots(T(1.e-5), yaxis_log=false, save_legend=false)
```

```julia

```


 Linear Regression - Bike Sharing Exercise

 ### Data Set Information
Bike sharing systems are new generation of traditional bike rentals where whole process from membership, rental and
return back has become automatic. Through these systems, user is able to easily rent a bike from a particular position
and return back at another position. Currently, there are about over 500 bike-sharing programs around the world which is
composed of over 500 thousands bicycles. Today, there exists great interest in these systems due to their important role
in traffic, environmental and health issues.
Apart from interesting real world applications of bike sharing systems, the characteristics of data being generated
by these systems make them attractive for the research. Opposed to other transport services such as bus or subway, the
duration of travel, departure and arrival position is explicitly recorded in these systems. This feature turns bike sharing
system into a virtual sensor network that can be used for sensing mobility in the city. Hence, it is expected that most of
important events in the city could be detected via monitoring these data.

### Loading and Plotting Data

```python
import pandas as pd
import numpy as np
import seaborn as sns
```

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

```python
plt.style.use('ggplot')
plt.rcParams['figure.figsize'] = (18, 6)
```

```python
data = pd.read_csv('data.csv')
data.head()
```

      instant
      dteday
      season
      yr
      mnth
      holiday
      weekday
      workingday
      weathersit
      temp
      atemp
      hum
      windspeed
      casual
      registered
      cnt

      0
      1
      01/01/2011
      1
      0
      1
      0
      6
      0
      2
      0.344167
      0.363625
      0.805833
      0.160446
      331
      654
      985

      1
      2
      02/01/2011
      1
      0
      1
      0
      0
      0
      2
      0.363478
      0.353739
      0.696087
      0.248539
      131
      670
      801

      2
      3
      03/01/2011
      1
      0
      1
      0
      1
      1
      1
      0.196364
      0.189405
      0.437273
      0.248309
      120
      1229
      1349

      3
      4
      04/01/2011
      1
      0
      1
      0
      2
      1
      1
      0.200000
      0.212122
      0.590435
      0.160296
      108
      1454
      1562

      4
      5
      05/01/2011
      1
      0
      1
      0
      3
      1
      1
      0.226957
      0.229270
      0.436957
      0.186900
      82
      1518
      1600

```python
data.info()
```

    <class 'pandas.core.frame.DataFrame'>
    RangeIndex: 731 entries, 0 to 730
    Data columns (total 16 columns):
     #   Column      Non-Null Count  Dtype
    ---  ------      --------------  -----
     0   instant     731 non-null    int64
     1   dteday      731 non-null    object
     2   season      731 non-null    int64
     3   yr          731 non-null    int64
     4   mnth        731 non-null    int64
     5   holiday     731 non-null    int64
     6   weekday     731 non-null    int64
     7   workingday  731 non-null    int64
     8   weathersit  731 non-null    int64
     9   temp        731 non-null    float64
     10  atemp       731 non-null    float64
     11  hum         731 non-null    float64
     12  windspeed   731 non-null    float64
     13  casual      731 non-null    int64
     14  registered  731 non-null    int64
     15  cnt         731 non-null    int64
    dtypes: float64(4), int64(11), object(1)
    memory usage: 91.5+ KB

```python
data.describe()
```

      instant
      season
      yr
      mnth
      holiday
      weekday
      workingday
      weathersit
      temp
      atemp
      hum
      windspeed
      casual
      registered
      cnt

      count
      731.000000
      731.000000
      731.000000
      731.000000
      731.000000
      731.000000
      731.000000
      731.000000
      731.000000
      731.000000
      731.000000
      731.000000
      731.000000
      731.000000
      731.000000

      mean
      366.000000
      2.496580
      0.500684
      6.519836
      0.028728
      2.997264
      0.683995
      1.395349
      0.495385
      0.474354
      0.627894
      0.190486
      848.176471
      3656.172367
      4504.348837

      std
      211.165812
      1.110807
      0.500342
      3.451913
      0.167155
      2.004787
      0.465233
      0.544894
      0.183051
      0.162961
      0.142429
      0.077498
      686.622488
      1560.256377
      1937.211452

      min
      1.000000
      1.000000
      0.000000
      1.000000
      0.000000
      0.000000
      0.000000
      1.000000
      0.059130
      0.079070
      0.000000
      0.022392
      2.000000
      20.000000
      22.000000

      25%
      183.500000
      2.000000
      0.000000
      4.000000
      0.000000
      1.000000
      0.000000
      1.000000
      0.337083
      0.337842
      0.520000
      0.134950
      315.500000
      2497.000000
      3152.000000

      50%
      366.000000
      3.000000
      1.000000
      7.000000
      0.000000
      3.000000
      1.000000
      1.000000
      0.498333
      0.486733
      0.626667
      0.180975
      713.000000
      3662.000000
      4548.000000

      75%
      548.500000
      3.000000
      1.000000
      10.000000
      0.000000
      5.000000
      1.000000
      2.000000
      0.655417
      0.608602
      0.730209
      0.233214
      1096.000000
      4776.500000
      5956.000000

      max
      731.000000
      4.000000
      1.000000
      12.000000
      1.000000
      6.000000
      1.000000
      3.000000
      0.861667
      0.840896
      0.972500
      0.507463
      3410.000000
      6946.000000
      8714.000000

```python
temps = data['atemp'].values
rentals = data['cnt'].values / 1000
```

### Plotting the Data

```python
plt.scatter(temps, rentals, marker = 'o', color = 'red')
plt.xlabel('Normalized Temperature in C')
plt.ylabel('Bike Rentals in 1000s')
plt.title('Bike Rentals Prediction');
```

### Simple Linear Regression

### Compute the Cost Function
The objective of linear regression is to minimize the cost function

$$J(\theta) = \frac{1}{2m} \sum_{i=1}^m (h_\theta(x^{(i)}) - y^{(i)} )^2$$

where $h_{\theta}(x)$ is the hypothesis and given by the linear model

$$h_{\theta}(x) = \theta^Tx = \theta_0 + \theta_1x_1$$

where $0$ is a vector of feature weights, $x^{(i)}$ is the $ith$ training example, $y^{(i)}$ is that example's $y$ value, and
$x_j$ is the value for its $jth$ fuature.

```python
def compute_cost(X, y, theta):
    return np.sum(np.square(np.matmul(X, theta) - y)) / (2 * len(y))
```

Before computing the cost with an initial guess for $θ$, a column of 1s is prepended onto the input data. This allows us to vectorize the cost function, as well as make it usable for multiple linear regression later. This first value $θ_0$ now behaves as a constant in the cost function.

```python
theta = np.zeros(2)
X = np.column_stack((np.ones(len(temps)), temps))
y = rentals
cost = compute_cost(X, y, theta)

print('theta', theta)
print('cost:', cost)
```

    theta [0. 0.]
    cost: 12.018406441176468

### Gradient Descent Algorithm

We’ll now minimize the cost using the gradient descent algorithm. Intuitively, gradient descent takes small, linear hops down the slope of a function in each feature dimension, with the size of each hop determined by the partial derivative of the cost function with respect to that feature and a learning rate multiplier $α$. If tuned properly, the algorithm converges on a global minimum by iteratively adjusting feature weights $θ$ of the cost function, as shown here for two feature dimensions:

\begin{align}
\theta_0 & := \theta_0 - \alpha\frac{\partial}{\partial\theta_0} J(\theta_0,\theta_1) \\
\theta_1 & := \theta_1 - \alpha\frac{\partial}{\partial\theta_1} J(\theta_0,\theta_1)
\end{align}

The update rule each iteration then becomes:

\begin{align}
\theta_0 & := \theta_0 - \alpha\frac{1}{m} \sum_{i=1}^m (h_\theta(x^{(i)})-y^{(i)}) \\
\theta_1 & := \theta_1 - \alpha\frac{1}{m} \sum_{i=1}^m (h_\theta(x^{(i)})-y^{(i)})x_1^{(i)} \\
\end{align}

```python
def gradient_descent(X, y, alpha, iterations):
    theta = np.zeros(2)
    m = len(y)

    for i in range(iterations):
        t0 = theta[0] - (alpha / m) * np.sum(np.dot(X, theta) - y)
        t1 = theta[1] - (alpha / m) * np.sum((np.dot(X, theta) - y) * X[:,1])
        theta = np.array([t0, t1])

    return theta

iterations = 5000
alpha = 0.1

theta = gradient_descent(X, y, alpha, iterations)
cost = compute_cost(X, y, theta)

print("theta:", theta)
print('cost:', compute_cost(X, y, theta))
```

    theta: [0.94588081 7.50171673]
    cost: 1.1275869258439812

We can examine the values of $\theta$ chosen by the algorithm using a few different visualizations, first by plotting $h_\theta(x)$ against the input data.  The results show the expected correlation between temperature and rentals.

```python
global str
plt.scatter(temps, rentals, marker='o', color='blue')
plt.xlabel('Normalized Temperature in C')
plt.ylabel('Bike Rentals in 1000s')
plt.title('Bike Rentals Prediction Fit')
samples = np.linspace(min(temps), max(temps))
plt.plot(samples, theta[0] + theta[1] * samples);
```

### Visualising the Cost Function $J(\Theta)$

A surface plot is a better illustration of how gradient descent approaches a global minimum, plotting the values for $\theta$ against their associated cost.

```python
from mpl_toolkits.mplot3d import Axes3D
from matplotlib import cm

Xs, Ys = np.meshgrid(np.linspace(-5, 5, 50), np.linspace(-40, 40, 50))
Zs = np.array([compute_cost(X, y, [t0, t1]) for t0, t1 in zip(np.ravel(Xs), np.ravel(Ys))])
Zs = np.reshape(Zs, Xs.shape)

fig = plt.figure(figsize=(15,10))
ax = fig.gca(projection="3d")
ax.set_xlabel(r'$\Theta0$')
ax.set_ylabel(r'$\Theta1$')
ax.set_zlabel(r'Cost')
ax.view_init(elev=25, azim=40)
ax.plot_surface(Xs, Ys, Zs, cmap=cm.rainbow);

#rotate for better angle
#ax.view_init(30,120)
```

Finally, a countour plot reveals slices of that surface plot in 2D space, and can show the resulting $\theta$ values sitting exactly at the global minimum.

```python
ax = plt.figure().gca()
ax.plot(theta[0], theta[1], 'r*')
plt.contour(Xs, Ys, Zs, np.logspace(-3, 3, 15));
```

Linear Regression - Bike Sharing Exercise/Linear Regression - Bike Sharing Exercise.ipynb


## Overview
In this notebook the full dataset is broken into 26 subsets for each of the unique values of the kinase key. A random forest is fit to each of the subsets and evaluated using $k=5$-fold cross validation. The purpose in doing this is to determine which of the kinases are more difficult to classify and to verify reports that the fgfr1 kinase may have "bad" data.

```python
import pandas as pd
import time
import glob
import numpy as np
from scipy.stats import randint as sp_randint
from prettytable import PrettyTable
from sklearn.preprocessing import Imputer
from sklearn.model_selection import train_test_split, cross_val_score, RandomizedSearchCV
from sklearn.metrics import f1_score, accuracy_score, make_scorer
from sklearn.ensemble import RandomForestClassifier
```

### Load the data
The data is distributed amongst 282  50mb .csv files, so the glob module is used to allow to read these files using a generic template path into a list, then the list elements are concated along the horizontal axis to create the full dataset. This takes about 10 minutes.

```python
load_data_t0 = time.clock()
df = pd.concat([pd.read_csv(filename, index_col=[1,0], na_values=['na'], engine='c', header=0) for filename in glob.glob("data/parser_output/csv/*.csv")],axis=0)
load_data_t1 = time.clock()
print ("data loaded in ~", ((load_data_t1 - load_data_t0)/60), "minutes.")
```

    data loaded in ~ 9.379375549999999 minutes.

after loading the data, extract the receptor names so that it is possible to form the seperate data subsets.

```python
receptor_names = list(df.index.get_level_values(0).unique())
```

Now iterate through the list of receptors and extract the corresponding subset of training data from the full dataframe. Keep in mind that the number of examples in each set are not the same, therefore the average f1 of each subset is stored into a list for later visualization. A random forest is fit to each of the subsets using $k=5$-fold cross validation with the scoring metric set to the f1-score in order to capture presence type I (precision) and type II (recall) errors. Accuracy is not used due to the fact that the data is imbalanced and that a good accuracy score may be misleading regarding the performance of the classifier on correctly classifying positive training/testing examples.

 \begin{equation} Precision = \frac{TP}{TP + FP} \end{equation}
 \begin{equation} Recall = \frac{TP}{TP + FN} \end{equation}
 \begin{equation} F1 =  2* \frac{Precision*Recall}{Precision + Recall} \end{equation}

## Comparison of Classification with Random Forest Optimized using Randomized Grid Search

```python
rforest_params = {"n_estimators": sp_randint(pow(2,5),pow(2,7))}
```

```python
cv_score_list = []
outputTable = PrettyTable()
outputTable.field_names = ["receptor","N","%positive","Mean F1","Min F1","Max F1"]
for receptor in receptor_names:
    receptor_df = df.iloc[df.index.get_level_values(0) == receptor]
    X = Imputer().fit_transform(receptor_df.drop('label', axis=1).as_matrix())
    y = pd.to_numeric(receptor_df['label']).as_matrix()
    #rforest = RandomizedSearchCV(RandomForestClassifier(oob_score=True, class_weight='balanced'),  rforest_params, cv = 3, scoring = make_scorer(f1_score),n_jobs=3)
    rforest = RandomForestClassifier(oob_score=True, class_weight='balanced',n_estimators=100)
    cv_score = cross_val_score(rforest,X,y,scoring='f1',cv=5)
    cv_score_list.append(np.mean(cv_score))
    outputTable.add_row([receptor,receptor_df.shape[0],(100*(y[y==1].shape[0]/y.shape[0])),np.mean(cv_score),np.min(cv_score),np.max(cv_score)])
    del rforest
    del X
    del y
print(outputTable)
```

    +----------+-------+--------------------+-----------------+----------------+-----------------+
    | receptor |   N   |     %positive      |     Mean F1     |     Min F1     |      Max F1     |
    +----------+-------+--------------------+-----------------+----------------+-----------------+
    |  vgfr2   | 25896 | 2.378745752239728  |  0.764598897734 | 0.712041884817 |       0.8       |
    |   abl1   | 11176 | 2.6216893342877596 |  0.651972471972 |      0.55      |  0.711111111111 |
    |   met    | 11674 | 2.0729826965907145 |  0.754497483605 |      0.6       |  0.843373493976 |
    |  mapk2   |  6448 | 3.163771712158809  |  0.70460783307  | 0.586206896552 |  0.876712328767 |
    |   akt1   | 16990 | 2.4779281930547383 |  0.86279303567  | 0.773722627737 |  0.930817610063 |
    |  tgfr1   |  8958 | 3.136860906452333  |  0.905018018505 | 0.836734693878 |  0.981818181818 |
    |   lck    | 28535 | 2.3865428421237076 |  0.752511780327 | 0.653465346535 |  0.824034334764 |
    |   src    | 35782 | 2.3084232295567606 |  0.806467372617 | 0.786764705882 |  0.825622775801 |
    |   braf   | 10349 | 2.425355106773601  |  0.743300990338 | 0.630136986301 |  0.876404494382 |
    |  rock1   |  6578 | 3.055640012161751  |  0.720354229178 | 0.59649122807  |  0.906666666667 |
    |  mp2k1   |  8481 | 2.829854969932791  |  0.848739066594 | 0.684931506849 |  0.921348314607 |
    |   jak2   |  6741 | 2.2400237353508383 |  0.658357916385 | 0.536585365854 |       0.8       |
    |   cdk2   | 29119 | 2.7164394381675194 |  0.636194383088 | 0.595555555556 |  0.699588477366 |
    |   mk14   | 37347 | 2.4499959836131415 |  0.719251783338 | 0.649446494465 |  0.785478547855 |
    |  csf1r   | 12717 | 2.2253676181489346 |  0.617376125294 | 0.465753424658 |  0.774193548387 |
    |   fak1   |  5514 | 2.0311933260790718 |  0.843805563806 | 0.685714285714 |  0.952380952381 |
    |   mk10   |  6895 | 2.668600435097897  |  0.572964513747 | 0.434782608696 |  0.678571428571 |
    |   mk01   |  4765 | 2.875131164742917  |  0.803874601706 | 0.666666666667 |       0.92      |
    |   egfr   | 36267 | 2.277552596023934  |  0.835043467827 | 0.795620437956 |  0.852233676976 |
    |   wee1   |  6371 | 2.1503688588918535 |  0.980688124306 | 0.941176470588 |       1.0       |
    |   akt2   |  7140 | 2.6330532212885154 |  0.723831003867 | 0.666666666667 |  0.786885245902 |
    |   plk1   |  7032 | 2.1757679180887375 |  0.671803640375 | 0.324324324324 |  0.909090909091 |
    |   kit    | 10858 | 2.2932400073678396 |  0.610776667822 | 0.461538461538 |  0.727272727273 |
    |  igf1r   |  9631 | 2.325822863669401  |  0.841302379595 | 0.724637681159 |  0.915662650602 |
    |  fgfr1   |  1460 | 32.73972602739726  | 0.0302027796765 |      0.0       | 0.0947368421053 |
    |   kpcb   |  9062 | 2.4166850584859856 |  0.810294515632 | 0.742857142857 |       0.9       |
    +----------+-------+--------------------+-----------------+----------------+-----------------+

## Visualize the Results
To get an idea of how the random forest tends to perform across the subsets of data, a violin plot is used to communicate the median and inner-quartile ranges of the data as well as visualize the estimated density of the samples at each point. As one can see the distribution is multimodal which implies some abnormality if we expect the f1-scores for any particular subset to lie within a single peaked gaussian.

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

plt.figure(figsize=[15,6])
plt.xlabel("mean_f1")
sns.violinplot(x=cv_score_list, cut=0)

print ("Mean F1:",np.mean(cv_score_list),"\tMin F1:",np.min(cv_score_list),"\tMax F1:",np.max(cv_score_list))
```

## Conclusions
As it can be observed, the f1-scores for the classifiers do vary when using the random forest with default settings. However a notable result is the fgfr1 receptor being very difficult to correctly classify with a markedly low score as compared to the other receptors. This implies that either the receptor is in general difficult to classify or that there are potential mistakes for the actives and decoys in the data from the DUDE dataset. Perhaps what is surprising is that the positive class makes up a larger portion of the training examples for the fgfr1 receptor than what is typically 2-3% for the other receptors.

notebooks/Evaluation of Random Forest on Separate Kinases.ipynb


# Running simulation with spin-transfer torque (STT)

**Purpose**: Show an example of a simulation with spin-transfer torque.

**Other topics touched**

**Author and creation date**: Weiwei Wang, Oct 2013

**Modifications by**:

The implemented equation in finmag with STT is [1,2],

\begin{equation}
\frac{\partial \vec{m}}{\partial t} = - \gamma \vec{m} \times \vec{H} + \alpha \vec{m} \times  \frac{\partial \vec{m}}{\partial t}  + u (\vec{j}_s \cdot \nabla) \vec{m} - \beta u [\vec{m}\times (\vec{j}_s \cdot \nabla)\vec{m}]
\end{equation}

where $\vec{j}_s$ is the current density. $u$ is the material parameter, and in default,

$$u=u_{ZL}=\frac{u_0}{1+\beta^2}$$

There is an option "using_u0" in sim.set_zhangli method, u=u0 if "using_u0 = True" and

$$u_0=\frac{g \mu_B P}{2 |e| M_s}=\frac{g \mu_B P a^3}{2 |e| \mu_s}$$

where $\mu_B=|e|\hbar/(2m)$ is the Bohr magneton, P is the polarization rate, e is the electron charge.

Notice that $\partial_x \vec{m} \cdot \vec{m}=0$ so $u_0 (\vec{j}_s \cdot \nabla) \vec{m}=  - u_0 \vec{m}\times[\vec{m}\times (\vec{j}_s \cdot \nabla)\vec{m}]$. Besides,
we can change the equation to atomistic one by introducing $\vec{s}=-\vec{S}$ where $\vec{S}$ is the local spin such that

$$\vec{M}=-\frac{g \mu_B}{a^3}\vec{S} =\frac{g \mu_B}{a^3}\vec{s}$$

so $u_0=p a^3/(2|e|s)$, furthermore,

$$
   \frac{\partial \vec{s}}{\partial t} = - \gamma \vec{s} \times \vec{H} + \frac{\alpha}{s} \vec{s} \times  \frac{\partial \vec{s}}{\partial t}   + \frac{p a^3}{2|e|s} (\vec{j}_s \cdot \nabla) \vec{s} -  \frac{p a^3 \beta}{2|e|s^2} [\vec{s}\times (\vec{j}_s \cdot \nabla)\vec{s}]
$$

[1] S. Zhang and Z. Li, Roles of nonequilibrium conduction electrons on the magnetization dynamics of ferromagnets, Phys. Rev. Lett. 93, 127204 (2004).

[2] A. Thiaville, Y. Nakatani, J. Miltat and Y. Suzuki, Micromagnetic understanding of current-driven domain wall motion in patterned nanowires, Europhys. Lett. 69, 990 (2005).

We give a short example on how to use the spin-transfer torque in finmag, first import the related python modules and create a 1d mesh,

```python
import dolfin as df
import numpy as np
from finmag import Simulation as Sim
from finmag.energies import Exchange, UniaxialAnisotropy
from finmag.util.helpers import set_logging_level
set_logging_level("INFO")

mesh=df.IntervalMesh(50,0,100)
```

    [2014-09-12 15:30:24] INFO: Finmag logging output will be appended to file: '/home/mb4e10/.finmag/global.log'
    [2014-09-12 15:30:24] DEBUG: Building modules in 'native'...
    [2014-09-12 15:30:25] DEBUG: FinMag          5450:432537721f7d2498ec9a103e0bdb00aadd7a57db
    [2014-09-12 15:30:25] DEBUG: Dolfin          1.4.0                Matplotlib      1.3.1
    [2014-09-12 15:30:25] DEBUG: Numpy           1.8.1                Scipy           0.14.0
    [2014-09-12 15:30:25] DEBUG: IPython         2.1.0                Python          2.7.6
    [2014-09-12 15:30:25] DEBUG: Paraview        4.0.1-1ubuntu1       Sundials        2.5.0
    [2014-09-12 15:30:25] DEBUG: Boost-Python    <unknown>            Linux           Ubuntu 14.04.1 LTS
    [2014-09-12 15:30:25] DEBUG: Registering debug signal handler. Press Ctrl-Z at any time to stop execution and jump into the debugger.

We define a function to generate the 1d domain wall profile,

```python
def init_m(pos):
    x=pos[0]

    delta = np.sqrt(13e-12/520e3)*1e9
    sx = -np.tanh((x-50)/delta)
    sy = 1/np.cosh((x-50)/delta)
    return (sx,sy,0)
```

Similarly, we define a function for the current density profile, here the current density is uniformly distributed along x direction,

```python
def init_J(pos):
    return (1e12,0,0)
```

We create a simulation instance and considering the exchange and anisotropy interactions,

```python
sim = Sim(mesh, Ms=8.6e5, unit_length=1e-9)

sim.set_m(init_m)

sim.add(UniaxialAnisotropy(K1=520e3, axis=[1, 0, 0]))
sim.add(Exchange(A=13e-12))
sim.alpha = 0.01
```

    [2014-09-12 15:30:29] INFO: Finmag logging output will be written to file: '/home/mb4e10/finmag/doc/ipython_notebooks_src/unnamed.log' (any old content will be overwritten).
    [2014-09-12 15:30:29] INFO: Creating Sim object 'unnamed' (rank=0/1).
    [2014-09-12 15:30:29] INFO: <Mesh of topological dimension 1 (intervals) with 51 vertices and 50 cells, ordered>

We use the spin-transfer torque with parameters polarisation=0.5 and beta=0.01

```python
sim.set_zhangli(init_J, 0.5, 0.01)
```

We run the simulation for 1e-10 s,

```python
m0 = sim.m.copy()
sim.run_until(1e-10)
m1 = sim.m.copy()
```

    [2014-09-12 15:30:31] INFO: Create integrator sundials with kwargs={}
    [2014-09-12 15:30:31] INFO: Simulation will run until t = 1e-10 s.
    [2014-09-12 15:30:32] INFO: Simulation has reached time t = 1e-10 s.

And finally, we plot the domain wall to see whether it moves or not,

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

m0.shape=(3,-1)
m1.shape=(3,-1)

plt.plot(m0[0], '.-', label='m_x_t=0')
plt.plot(m0[1], '.-', label='m_y_t=0')
plt.plot(m1[0], '^-', label='m_x_1e-10')
plt.plot(m1[1], '^-', label='m_y_t=1e-10')
plt.xlabel('x (nm)')
plt.ylabel('m')
plt.legend()
```