#  CS109B Data Science 2: Advanced Topics in Data Science

## Lab 4 - Bayesian Analysis

**Harvard University**

**Spring 2020**

**Instructors:** Mark Glickman, Pavlos Protopapas, and Chris Tanner

**Lab Instructors:** Chris Tanner and Eleni Angelaki Kaxiras

**Content:** Eleni Angelaki Kaxiras

---

```python
## RUN THIS CELL TO PROPERLY HIGHLIGHT THE EXERCISES
import requests
from IPython.core.display import HTML
styles = requests.get("https://raw.githubusercontent.com/Harvard-IACS/2019-CS109B/master/content/styles/cs109.css").text
HTML(styles)
```

```python
import pymc3 as pm
from pymc3 import summary
```

    WARNING (theano.tensor.blas): Using NumPy C-API based implementation for BLAS functions.

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

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

```python
print('Running on PyMC3 v{}'.format(pm.__version__))
```

    Running on PyMC3 v3.8

```javascript
%%javascript
IPython.OutputArea.auto_scroll_threshold = 20000;
```

    <IPython.core.display.Javascript object>

## Learning Objectives

By the end of this lab, you should be able to:
* Understand how probability distributions work.
* Apply Bayes Rule in calculating probabilities.
* Understand how to apply Bayesian analysis using PyMC3
* Avoid getting fired when talking to your Bayesian employer.

**This lab corresponds to Lectures 6, 7, and 8, and maps to Homework 3.**

## Table of Contents

1. The Bayesian Way of Thinking or Is this a Fair Coin?
2. [Intro to `pyMC3`](#pymc3).
3. [Bayesian Linear Regression](#blr).
4. [Try this at Home: Example on Mining Disasters](#no4).

## 1. The Bayesian way of Thinking

```
Here is my state of knowledge about the situation. Here is some data, I am now going to revise my state of knowledge.
```

Table Exercise: Discuss the statement above with your table mates and make sure everyone understands what it means and what constitutes Bayesian way of thinking. Finally, count the Bayesians among you.

### A. Bayes Rule

\begin{equation}
\label{eq:bayes}
P(A|\textbf{B}) = \frac{P(\textbf{B} |A) P(A) }{P(\textbf{B})}
\end{equation}

$P(A|\textbf{B})$ is the **posterior** distribution, prob(hypothesis | data)

$P(\textbf{B} |A)$ is the **likelihood** function, how probable is my data **B** for different values of the parameters

$P(A)$ is the marginal probability to observe the data, called the **prior**, this captures our belief about the data before observing it.

$P(\textbf{B})$ is the marginal distribution (sometimes called marginal likelihood)

Table Exercise: Solve the Monty Hall Paradox using Bayes Rule.

You are invited to play a game. There are 3 doors behind **one** of which are the keys to a brand new red Tesla. There is a goat behind each of the other two.

You are asked to pick one door, and let's say you pick **Door1**. The host knows where the keys are. Of the two remaining closed doors, he will always open the door that has a goat behind it. He'll say "I will do you a favor and open **Door2**". So he opens Door2 inside which there is, of course, a goat. He now asks you, do you want to open the initial Door you chose or change to **Door3**? Generally, in this game, when you are presented with this choice should you swap the doors?

**Initial Steps:**
- Start by defining the `events` of this probabilities game. One definition is:

    - $A_i$: car is behind door $i$

    - $B_i$ host opens door $i$

$i\in[1,2,3]$

- In more math terms, the question is: is the probability that the price is behind **Door 1** higher than the probability that the price is behind **Door2**, given that an event **has occured**?

### B. Bayes Rule written with Probability Distributions

We have data that we believe come from an underlying distribution of unknown parameters. If we find those parameters, we know everything about the process that generated this data and we can make inferences (create new data).

\begin{equation}
\label{eq:bayes}
P(\theta|\textbf{D}) = \frac{P(\textbf{D} |\theta) P(\theta) }{P(\textbf{D})}
\end{equation}

#### But what is $\theta \;$?

$\theta$ is an unknown yet fixed set of parameters. In Bayesian inference we express our belief about what $\theta$ might be and instead of trying to guess $\theta$ exactly, we look for its **probability distribution**. What that means is that we are looking for the **parameters** of that distribution. For example, for a Poisson distribution our $\theta$ is only $\lambda$. In a normal distribution, our $\theta$ is often just $\mu$ and $\sigma$.

### C. A review of Common Probability Distributions

#### Discrete Distributions

The random variable has a **probability mass function (pmf)** which measures the probability that our random variable will take a specific value $y$, denoted $P(Y=y)$.

- **Bernoulli** (binary outcome, success has probability $\theta$, $one$ trial):
$
P(Y=k) =  \theta^k(1-\theta)^{1-k}
$

- **Binomial** (binary outcome, success has probability $\theta$, $n$ trials):
\begin{equation}
P(Y=k) =  {{n}\choose{k}} \cdot \theta^k(1-\theta)^{n-k}
\end{equation}

*Note*: Binomial(1,$p$) = Bernouli($p$)

- **Negative Binomial**

- **Poisson** (counts independent events occurring at a rate)
\begin{equation}
P\left( Y=y|\lambda \right) = \frac{{e^{ - \lambda } \lambda ^y }}{{y!}}
\end{equation}
y = 0,1,2,...

- **Discrete Uniform**

- **Categorical, or Multinulli** (random variables can take any of K possible categories, each having its own probability; this is a generalization of the Bernoulli distribution for a discrete variable with more than two possible outcomes, such as the roll of a die)

- **Dirichlet-multinomial** (a generalization of the beta distribution for many variables)

#### Continuous Distributions

The random variable has a **probability density function (pdf)**.
- **Uniform** (variable equally likely to be near each value in interval $(a,b)$)
\begin{equation}
P(X = x) = \frac{1}{b - a}
\end{equation}
anywhere within the interval $(a, b)$, and zero elsewhere.

- **Normal** (a.k.a. Gaussian)
\begin{equation}
X \sim  \mathcal{N}(\mu,\,\sigma^{2})
\end{equation}

    A Normal distribution can be parameterized either in terms of precision $\tau$ or standard deviation ($\sigma^{2}$. The link between the two is given by
\begin{equation}
\tau = \frac{1}{\sigma^{2}}
\end{equation}
 - Mean $\mu$
 - Variance $\frac{1}{\tau}$ or $\sigma^{2}$
 - Parameters: `mu: float`, `sigma: float` or `tau: float`

- **Beta** (variable ($\theta$) taking on values in the interval $[0,1]$, and parametrized by two positive parameters, $\alpha$ and $\beta$ that control the shape of the distribution.

*Note:*Beta is a good distribution to use for priors (beliefs) because its range is $[0,1]$ which is the natural range for a probability and because we can model a wide range of functions by changing the $\alpha$ and $\beta$ parameters.

\begin{equation}
\label{eq:beta}
P(\theta) = \frac{1}{B(\alpha, \beta)} {\theta}^{\alpha - 1} (1 - \theta)^{\beta - 1} \propto {\theta}^{\alpha - 1} (1 - \theta)^{\beta - 1}
\end{equation}

where the normalisation constant, $B$, is a beta function of $\alpha$ and $\beta$,

\begin{equation}
B(\alpha, \beta) = \int_{t=0}^1 t^{\alpha - 1} (1 - t)^{\beta - 1} dt.
\end{equation}

- **Exponential**

- **Gamma**

 #### Code Resources:
 - Statistical Distributions in numpy/scipy: [scipy.stats](https://docs.scipy.org/doc/scipy/reference/stats.html)
 - Statistical Distributions in pyMC3: [distributions in PyMC3](https://docs.pymc.io/api/distributions.html) (we will see those below).

Exercise: Plot a Discrete variable

Change the value of $\mu$ in the Poisson PMF and see how the plot changes. Remember that the y-axis in a discrete probability distribution shows the probability of the random variable having a specific value in the x-axis.

\begin{equation}
P\left( X=k \right) = \frac{{e^{ - \mu } \mu ^k }}{{k!}}
\end{equation}

**stats.poisson.pmf(x, mu)** $\mu$(mu) is our $\theta$ in this case.

```python
plt.style.use('seaborn-darkgrid')
x = np.arange(0, 30)
for m in [0.5, 3, 8]:
    pmf = stats.poisson.pmf(x, m)
    plt.plot(x, pmf, 'o', alpha=0.5, label='$\mu$ = {}'.format(m))
plt.xlabel('random variable', fontsize=12)
plt.ylabel('probability', fontsize=12)
plt.legend(loc=1)
plt.ylim=(-0.1)
plt.show()
```

```python
# same for binomial
plt.style.use('seaborn-darkgrid')
x = np.arange(0, 22)
ns = [10, 17]
ps = [0.5, 0.7]
for n, p in zip(ns, ps):
    pmf = stats.binom.pmf(x, n, p)
    plt.plot(x, pmf, 'o', alpha=0.5, label='n = {}, p = {}'.format(n, p))
plt.xlabel('x', fontsize=14)
plt.ylabel('f(x)', fontsize=14)
plt.legend(loc=1)
plt.show()
```

```python
# discrete uniform
plt.style.use('seaborn-darkgrid')
ls = [0]
us = [3] # watch out, this number can only be integer!
for l, u in zip(ls, us):
    x = np.arange(l, u+1)
    pmf = [1.0 / (u - l + 1)] * len(x)
    plt.plot(x, pmf, '-o', label='lower = {}, upper = {}'.format(l, u))
plt.xlabel('x', fontsize=12)
plt.ylabel('probability P(x)', fontsize=12)
plt.legend(loc=1)
plt.show()
```

Exercise: Plot a continuous variable

Change the value of $\mu$ in the Uniform PDF and see how the plot changes.

Remember that the y-axis in a continuous probability distribution does not shows the actual probability of the random variable having a specific value in the x-axis because that probability is zero!. Instead, to see the probability that the variable is within a small margin we look at the integral below the curve of the PDF.

The uniform is often used as a noninformative prior.

```
Uniform - numpy.random.uniform(a=0.0, b=1.0, size)
```

$\alpha$ and $\beta$ are our parameters. `size` is how many tries to perform.
Our $\theta$ is basically the combination of the parameters a,b. We can also call it
\begin{equation}
\mu = (a+b)/2
\end{equation}

```python
from scipy.stats import uniform

r = uniform.rvs(size=1000)
plt.plot(r, uniform.pdf(r),'r-', lw=5, alpha=0.6, label='uniform pdf')
plt.hist(r, density=True, histtype='stepfilled', alpha=0.2)
plt.ylabel(r'probability density')
plt.xlabel(f'random variable')
plt.legend(loc='best', frameon=False)
plt.show()
```

```python
from scipy.stats import beta

alphas = [0.5, 1.5, 3.0]
betas = [0.5, 1.5, 3.0]
x = np.linspace(0, 1, 1000)
colors = ['red', 'green', 'blue']

fig, ax = plt.subplots(figsize=(8, 5))

for a, b, colors in zip(alphas, betas, colors):
    dist = beta(a, b)
    plt.plot(x, dist.pdf(x), c=colors,
             label=f'a={a}, b={b}')

ax.set_ylim(0, 3)

ax.set_xlabel(r'$\theta$')
ax.set_ylabel(r'$p(\theta|\alpha,\beta)$')
ax.set_title('Beta Distribution')

ax.legend(loc='best')
fig.show();
```

```python
plt.style.use('seaborn-darkgrid')
x = np.linspace(-5, 5, 1000)
mus = [0., 0., 0., -2.]
sigmas = [0.4, 1., 2., 0.4]
for mu, sigma in zip(mus, sigmas):
    pdf = stats.norm.pdf(x, mu, sigma)
    plt.plot(x, pdf, label=r'$\mu$ = '+ f'{mu},' + r'$\sigma$ = ' + f'{sigma}')
plt.xlabel('random variable', fontsize=12)
plt.ylabel('probability density', fontsize=12)
plt.legend(loc=1)
plt.show()
```

```python
plt.style.use('seaborn-darkgrid')
x = np.linspace(-5, 5, 1000)
mus = [0., 0., 0., -2.] # mean
sigmas = [0.4, 1., 2., 0.4] # std
for mu, sigma in zip(mus, sigmas):
      plt.plot(x, uniform.pdf(x, mu, sigma), lw=5, alpha=0.4, \
               label=r'$\mu$ = '+ f'{mu},' + r'$\sigma$ = ' + f'{sigma}')
plt.xlabel('random variable', fontsize=12)
plt.ylabel('probability density', fontsize=12)
plt.legend(loc=1)
plt.show()
```

### D. Is this a Fair Coin?

We do not want to promote gambling but let's say you visit the casino in **Monte Carlo**. You want to test your theory that casinos are dubious places where coins have been manipulated to have a larger probability for tails. So you will try to estimate how fair a coin is based on 100 flips.

You begin by flipping the coin. You get either Heads ($H$) or Tails ($T$) as our observed data and want to see if your posterior probabilities change as you obtain more data, that is, more coin flips. A nice way to visualize this is to plot the posterior probabilities as we observe more flips (data).

We will be using Bayes rule. $\textbf{D}$ is our data.

\begin{equation}
\label{eq:bayes}
P(\theta|\textbf{D}) = \frac{P(\textbf{D} |\theta) P(\theta) }{P(\textbf{D})}
\end{equation}

In the case of a coin toss when we observe $k$ heads in $n$ tosses:
\begin{equation}
\label{eq:bayes}
P(\theta|\textbf{k}) = Beta(\alpha + \textbf{k}, \beta + n - \textbf{k})
\end{equation}

we can say that $\alpha$ and $\beta$ play the roles of a "prior number of heads" and "prior number of tails".

```python
# play with the priors - here we manually set them but we could be sampling from a separate Beta
trials = np.array([0, 1, 3, 5, 10, 15, 20, 100, 200, 300])
heads = np.array([0, 1, 2, 4, 8, 10, 10, 50, 180, 150])
x = np.linspace(0, 1, 100)

# for simplicity we set a,b=1

plt.figure(figsize=(10,8))
for k, N in enumerate(trials):
    sx = plt.subplot(len(trials)/2, 2, k+1)
    posterior = stats.beta.pdf(x, 1 + heads[k], 1 + trials[k] - heads[k])
    plt.plot(x, posterior, alpha = 0.5, label=f'{trials[k]} tosses\n {heads[k]} heads');
    plt.fill_between(x, 0, posterior, color="#348ABD", alpha=0.4)
    plt.legend(loc='upper left', fontsize=10)
    plt.legend()
    plt.autoscale(tight=True)

plt.suptitle("Posterior probabilities for coin flips", fontsize=15);
plt.tight_layout()
plt.subplots_adjust(top=0.88)
```

 [Top](#top)

## 2. Introduction to `pyMC3`

PyMC3 is a Python library for programming Bayesian analysis, and more specifically, data creation, model definition, model fitting, and posterior analysis. It uses the concept of a `model` which contains assigned parametric statistical distributions to unknown quantities in the model. Within models we define random variables and their distributions. A distribution requires at least a `name` argument, and other `parameters` that define it. You may also use the `logp()` method in the model to build the model log-likelihood function. We define and fit the model.

PyMC3 includes a comprehensive set of pre-defined statistical distributions that can be used as model building blocks. Although they are not meant to be used outside of a `model`, you can invoke them by using the prefix `pm`, as in `pm.Normal`.

#### Markov Chain Monte Carlo (MCMC) Simulations

PyMC3 uses the **No-U-Turn Sampler (NUTS)** and the **Random Walk Metropolis**, two Markov chain Monte Carlo (MCMC) algorithms for sampling in posterior space. Monte Carlo gets into the name because when we sample in posterior space, we choose our next move via a pseudo-random process. NUTS is a sophisticated algorithm that can handle a large number of unknown (albeit continuous) variables.

```python
with pm.Model() as model:
    z = pm.Normal('z', mu=0., sigma=5.)
    x = pm.Normal('x', mu=z, sigma=1., observed=5.)
print(x.logp({'z': 2.5}))
print(z.random(10, 100)[:10])
```

    -4.043938533204672
    [-1.86724707  1.53445629  6.85710407  0.82954017 -9.17174083  3.78352066
      1.59285816 -2.32735206 -7.08616453 -3.37588431]

**References**:

- *Salvatier J, Wiecki TV, Fonnesbeck C. 2016. Probabilistic programming in Python using PyMC3. PeerJ Computer Science 2:e55* [(https://doi.org/10.7717/peerj-cs.55)](https://doi.org/10.7717/peerj-cs.55)
- [Distributions in PyMC3](https://docs.pymc.io/api/distributions.html)
- [More Details on Distributions](https://docs.pymc.io/developer_guide.html)

Information about PyMC3 functions including descriptions of distributions, sampling methods, and other functions, is available via the `help` command.

```python
help(pm.Poisson)
```

    Help on class Poisson in module pymc3.distributions.discrete:

    class Poisson(pymc3.distributions.distribution.Discrete)
     |  Poisson(name, *args, **kwargs)
     |
     |  Poisson log-likelihood.
     |
     |  Often used to model the number of events occurring in a fixed period
     |  of time when the times at which events occur are independent.
     |  The pmf of this distribution is
     |
     |  .. math:: f(x \mid \mu) = \frac{e^{-\mu}\mu^x}{x!}
     |
     |  .. plot::
     |
     |      import matplotlib.pyplot as plt
     |      import numpy as np
     |      import scipy.stats as st
     |      plt.style.use('seaborn-darkgrid')
     |      x = np.arange(0, 15)
     |      for m in [0.5, 3, 8]:
     |          pmf = st.poisson.pmf(x, m)
     |          plt.plot(x, pmf, '-o', label='$\mu$ = {}'.format(m))
     |      plt.xlabel('x', fontsize=12)
     |      plt.ylabel('f(x)', fontsize=12)
     |      plt.ylim(0)
     |      plt.legend(loc=1)
     |      plt.show()
     |
     |  ========  ==========================
     |  Support   :math:`x \in \mathbb{N}_0`
     |  Mean      :math:`\mu`
     |  Variance  :math:`\mu`
     |  ========  ==========================
     |
     |  Parameters
     |  ----------
     |  mu : float
     |      Expected number of occurrences during the given interval
     |      (mu >= 0).
     |
     |  Notes
     |  -----
     |  The Poisson distribution can be derived as a limiting case of the
     |  binomial distribution.
     |
     |  Method resolution order:
     |      Poisson
     |      pymc3.distributions.distribution.Discrete
     |      pymc3.distributions.distribution.Distribution
     |      builtins.object
     |
     |  Methods defined here:
     |
     |  __init__(self, mu, *args, **kwargs)
     |      Initialize self.  See help(type(self)) for accurate signature.
     |
     |  logp(self, value)
     |      Calculate log-probability of Poisson distribution at specified value.
     |
     |      Parameters
     |      ----------
     |      value : numeric
     |          Value(s) for which log-probability is calculated. If the log probabilities for multiple
     |          values are desired the values must be provided in a numpy array or theano tensor
     |
     |      Returns
     |      -------
     |      TensorVariable
     |
     |  random(self, point=None, size=None)
     |      Draw random values from Poisson distribution.
     |
     |      Parameters
     |      ----------
     |      point : dict, optional
     |          Dict of variable values on which random values are to be
     |          conditioned (uses default point if not specified).
     |      size : int, optional
     |          Desired size of random sample (returns one sample if not
     |          specified).
     |
     |      Returns
     |      -------
     |      array
     |
     |  ----------------------------------------------------------------------
     |  Methods inherited from pymc3.distributions.distribution.Distribution:
     |
     |  __getnewargs__(self)
     |
     |  __latex__ = _repr_latex_(self, name=None, dist=None)
     |      Magic method name for IPython to use for LaTeX formatting.
     |
     |  default(self)
     |
     |  get_test_val(self, val, defaults)
     |
     |  getattr_value(self, val)
     |
     |  logp_nojac(self, *args, **kwargs)
     |      Return the logp, but do not include a jacobian term for transforms.
     |
     |      If we use different parametrizations for the same distribution, we
     |      need to add the determinant of the jacobian of the transformation
     |      to make sure the densities still describe the same distribution.
     |      However, MAP estimates are not invariant with respect to the
     |      parametrization, we need to exclude the jacobian terms in this case.
     |
     |      This function should be overwritten in base classes for transformed
     |      distributions.
     |
     |  logp_sum(self, *args, **kwargs)
     |      Return the sum of the logp values for the given observations.
     |
     |      Subclasses can use this to improve the speed of logp evaluations
     |      if only the sum of the logp values is needed.
     |
     |  ----------------------------------------------------------------------
     |  Class methods inherited from pymc3.distributions.distribution.Distribution:
     |
     |  dist(*args, **kwargs) from builtins.type
     |
     |  ----------------------------------------------------------------------
     |  Static methods inherited from pymc3.distributions.distribution.Distribution:
     |
     |  __new__(cls, name, *args, **kwargs)
     |      Create and return a new object.  See help(type) for accurate signature.
     |
     |  ----------------------------------------------------------------------
     |  Data descriptors inherited from pymc3.distributions.distribution.Distribution:
     |
     |  __dict__
     |      dictionary for instance variables (if defined)
     |
     |  __weakref__
     |      list of weak references to the object (if defined)

 [Top](#top)

## 3. Bayesian Linear Regression

Let's say we want to predict outcomes Y as normally distributed observations with an expected value $mu$ that is a linear function of two predictor variables, $\bf{x}_1$ and $\bf{x}_2$.

\begin{equation}
\mu = \alpha + \beta_1 \bf{x}_1 + \beta_2 x_2
\end{equation}

\begin{equation}
Y \sim  \mathcal{N}(\mu,\,\sigma^{2})
\end{equation}

where $\sigma^2$ represents the measurement error.

In this example, we will use $\sigma^2 = 10$

We also choose the parameters as normal distributions:

\begin{eqnarray}
\alpha \sim  \mathcal{N}(0,\,10) \\
\beta_i \sim  \mathcal{N}(0,\,10) \\
\sigma^2 \sim  |\mathcal{N}(0,\,10)|
\end{eqnarray}

We will artificially create the data to predict on. We will then see if our model predicts them correctly.

```python
# Initialize random number generator
np.random.seed(123)

# True parameter values
alpha, sigma = 1, 1
beta = [1, 2.5]

# Size of dataset
size = 100

# Predictor variable
X1 = np.linspace(0, 1, size)
X2 = np.linspace(0,.2, size)

# Simulate outcome variable
Y = alpha + beta[0]*X1 + beta[1]*X2 + np.random.randn(size)*sigma

fig, ax = plt.subplots(1,2, figsize=(10,6), sharex=True)
ax[0].scatter(X1,Y)
ax[1].scatter(X2,Y)
ax[0].set_xlabel(r'$x_1$', fontsize=14)
ax[0].set_ylabel(r'$Y$', fontsize=14)
ax[1].set_xlabel(r'$x_2$', fontsize=14)
ax[1].set_ylabel(r'$Y$', fontsize=14)
```

```python
from pymc3 import Model, Normal, HalfNormal

basic_model = Model()

with basic_model:

    # Priors for unknown model parameters, specifically create stochastic random variables
    # with Normal prior distributions for the regression coefficients,
    # and a half-normal distribution for the standard deviation of the observations, σ.
    alpha = Normal('alpha', mu=0, sd=10)
    beta = Normal('beta', mu=0, sd=10, shape=2)
    sigma = HalfNormal('sigma', sd=1)

    # Expected value of outcome - posterior
    mu = alpha + beta[0]*X1 + beta[1]*X2   # could be called y_hat, which is the expected value of y

    # Likelihood (sampling distribution) of observations
    Y_obs = Normal('Y_obs', mu=mu, sd=sigma, observed=Y)
```

```python
# model fitting with sampling
from pymc3 import NUTS, sample, find_MAP
from scipy import optimize

with basic_model:

    # obtain starting values via MAP
    start = find_MAP(fmin=optimize.fmin_powell)

    # instantiate sampler
    step = NUTS(scaling=start)

    # draw 2000 posterior samples
    trace = sample(2000, step, start=start)
```

```python
from pymc3 import traceplot

traceplot(trace);
```

```python
results = pm.summary(trace,
                     var_names=['alpha', 'beta', 'sigma'])
results
```

This linear regression example is from the original paper on PyMC3: *Salvatier J, Wiecki TV, Fonnesbeck C. 2016. Probabilistic programming in Python using PyMC3. PeerJ Computer Science 2:e55 https://doi.org/10.7717/peerj-cs.55*

 [Top](#top)

## 4. Try this at Home: Example on Mining Disasters
We will go over the classical `mining disasters from 1851 to 1962` dataset.

This example is from the [pyMC3 Docs](https://docs.pymc.io/notebooks/getting_started.html).

```python
import pandas as pd
disaster_data = pd.Series([4, 5, 4, 0, 1, 4, 3, 4, 0, 6, 3, 3, 4, 0, 2, 6,
                           3, 3, 5, 4, 5, 3, 1, 4, 4, 1, 5, 5, 3, 4, 2, 5,
                           2, 2, 3, 4, 2, 1, 3, np.nan, 2, 1, 1, 1, 1, 3, 0, 0,
                           1, 0, 1, 1, 0, 0, 3, 1, 0, 3, 2, 2, 0, 1, 1, 1,
                           0, 1, 0, 1, 0, 0, 0, 2, 1, 0, 0, 0, 1, 1, 0, 2,
                           3, 3, 1, np.nan, 2, 1, 1, 1, 1, 2, 4, 2, 0, 0, 1, 4,
                           0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1])
fontsize = 12
years = np.arange(1851, 1962)
plt.figure(figsize=(10,5))
#plt.scatter(years, disaster_data);
plt.bar(years, disaster_data)
plt.ylabel('Disaster count', size=fontsize)
plt.xlabel('Year', size=fontsize);
plt.title('Was there a Turning Point in Mining disasters from 1851 to 1962?', size=15);
```

#### Building the model

**Step1:** We choose the probability model for our experiment. Occurrences of disasters in the time series is thought to follow a **Poisson** process with a large **rate** parameter in the early part of the time series, and from one with a smaller **rate** in the later part. We are interested in locating the change point in the series, which perhaps is related to changes in mining safety regulations.

```
disasters = pm.Poisson('disasters', rate, observed=disaster_data)
```

We have two rates, `early_rate` if $t<=s$, and `late_rate` if $t>s$, where $s$ is the year the switch was made (a.k.a. the `switchpoint`).

**Step2:** Choose a prior distributions of the two rates, what we believe the rates were before we observed the data, and the switchpoint. We choose Exponential.
```
early_rate = pm.Exponential('early_rate', 1)
```

The parameters of this model are:

**Note:** Watch for missing values. Missing values are handled transparently by passing a MaskedArray or a pandas.DataFrame. Behind the scenes, another random variable, disasters.missing_values is created to model the missing values. If you pass a np.array with missing values you will get an error.

```python
with pm.Model() as disaster_model:

    # discrete
    switchpoint = pm.DiscreteUniform('switchpoint', lower=years.min(), upper=years.max(), testval=1900)

    # Priors for pre- and post-switch rates number of disasters
    early_rate = pm.Exponential('early_rate', 1)
    late_rate = pm.Exponential('late_rate', 1)

    # our theta - allocate appropriate Poisson rates to years before and after current
    # switch is an `if` statement in puMC3
    rate = pm.math.switch(switchpoint >= years, early_rate, late_rate)

    # our observed data as a likelihood function of the `rate` parameters
    # shows how we think our data is distributed
    disasters = pm.Poisson('disasters', rate, observed=disaster_data)
```

#### Model Fitting

```python
# there are defaults but we can also more explicitly set the sampling algorithms
with disaster_model:

    # for continuous variables
    step1 = pm.NUTS([early_rate, late_rate])

    # for discrete variables
    step2 = pm.Metropolis([switchpoint, disasters.missing_values[0]] )

    trace = pm.sample(10000, step=[step1, step2])
    # try different number of samples
    #trace = pm.sample(5000, step=[step1, step2])
```

#### Posterior Analysis

On the left side plots we notice that our early rate is between 2.5 and 3.5 disasters a year. In the late period it seems to be between 0.6 and 1.2 so definitely lower.

The right side plots show the samples we drew to come to our conclusion.

```python
pm.traceplot(trace, ['early_rate', 'late_rate', 'switchpoint'], figsize=(20,10));
```

```python
results = pm.summary(trace,
                     var_names=['early_rate', 'late_rate', 'switchpoint'])
results
```


# Chemical Reactions
The simulation procedure described in the previous chapter is now applied to a series of simple examples that represent chemical reactions. We first remind the reader of some key properties of chemical reactions that will show up in dynamic simulations and determine characteristics of network dynamic responses. We then go through a set of examples of chemical reactions that occur in a _closed system_. A closed system is isolated from its environment. No molecules enter or leave the system. Reactions being carried out in the laboratory in a sealed container represent an example of closed systems. In this chapter we assign numerical values to all the parameters for illustration purposes. We start by importing **MASSpy**:

```python
from mass import (
    MassModel, MassMetabolite, MassReaction, Simulation)
from mass.visualization import plot_time_profile, plot_phase_portrait
```

Other useful packages are also imported at this time.

```python
import numpy as np
import matplotlib.pyplot as plt
```

## Basic Properties of Reactions
Links between molecular components in a biochemical reaction network are given by chemical reactions or associations between chemical components. These links are therefore characterized and constrained by basic chemical rules.

### Bi-linear reactions are prevalent in biology
Although there are linear reactions found in biological reaction networks, the prototypical transformations in living systems at the molecular level are bi-linear. This association involves two compounds coming together to either be chemically transformed through the breakage and formation of a new covalent bond, as is typical of metabolic reactions or macromolecular synthesis,

$$\begin{equation*} \text{X} +\text{Y} \rightleftharpoons \text{X-Y}\ \text{covalent bonds} \end{equation*}$$

or two molecules associated together to form a complex that may be held together by hydrogen bonds and/or other physical association forces to form a complex that has a different functionality than individual components,

$$\begin{equation*}\text{X} +\text{Y} \rightleftharpoons \text{X:Y}\ \text{association of molecules} \end{equation*}$$

Such association, for instance, could designate the binding of a transcription factor to DNA to form an activated site to which an activated polymerase binds. Such bi-linear association between two molecules might also involve the binding of an allosteric regulator to an allosteric enzyme that induces a conformational change in the enzyme.

### Properties of biochemical reactions
Chemical transformations have three key properties that will influence the dynamic features of reaction networks and how we interpret dynamic states:

#### Stoichiometry
The stoichiometry of chemical reactions is fixed and is described by integral numbers counting the molecules that react and that form as a consequence of the chemical reaction. Thus, stoichiometry basically represents "digital information." Chemical transformations are constrained by elemental and charge balancing, as well as other features. Stoichiometry is invariant between organisms for the same reactions and does not change with pressure, temperature, or other conditions. Stoichiometry gives the primary topological properties of a biochemical reaction network.

#### Thermodynamics
All reactions inside a cell are governed by thermodynamics that determine the equilibrium state of a reaction. The relative rates of the forward and reverse reactions are therefore fixed by basic thermodynamic properties. Unlike stoichiometry, thermodynamic properties do change with physico-chemical conditions such as pressure and temperature. Thus the thermodynamics of transformation between small molecules in cells are fixed but condition-dependent. The thermodynamic properties of associations between macromolecules can be changed by altering the amino acid sequence of a protein or by phosphorylation of amino acids in the interface region, or by conformational change induced by the binding of a small molecule ligand.

#### Absolute Rates
In contrast to stoichiometry and thermodynamics, the absolute rates of chemical reactions inside cells are highly manipulable. Highly evolved enzymes are very specific in catalyzing particular chemical transformations. Cells can thus extensively manipulate the absolute rates of reactions through changes in their DNA sequence.

All biochemical transformations are subject to the basic rules of chemistry and thermodynamics.

## The Reversible Linear Reaction
We start with the reversible linear reaction:

$$\begin{equation} x_1 \underset{v_{-1}}{\stackrel{v_1}{\rightleftharpoons}} x_2 \tag{4.1} \end{equation}$$

Here we have that

$$\begin{equation*} \textbf{S} = \begin{pmatrix} {-1} & {1} \\ {1}  & {-1} \\ \end{pmatrix}, \textbf{v}(\textbf{x}) = \begin{pmatrix} {v_1(x_1)} \\ {v_{-1}(x_2)} \end{pmatrix} = \begin{pmatrix} {k_1x_1} \\ {k_{-1}x_2} \end{pmatrix} \end{equation*}$$

and thus the differential equations that we will need to simulate are:

$$\begin{equation} \frac{dx_1}{dt} = -k_1x_1 + k_{-1}x_2, \frac{dx_2}{dt} = k_1x_1 - k_{-1}x_2 = -\frac{dx_1}{dt} \tag{4.2} \end{equation}$$

with the reaction rate given as the difference between two elementary reaction rates

$$\begin{equation} v_{1, net} = v_1 - v_{-1} = k_1x_1 - k_{-1}x_2 = k_1(x_1 - x_2/K_1) \tag{4.3} \end{equation}$$

where $K_1 = k_1/k_{-1} = x_{2, eq}/x_{1, eq}$ or the ratio of the product to reactant concentrations at equilibrium, the conventional definition of an equilibrium constant in chemistry. Note that in Eq. (4.3), $k_1$ represents the kinetics, or the rate of change, while $(x_1 - x_2/K_1)$ represents the thermodynamics measuring how far from equilibrium the system is, i.e.,$(x_{1, eq} - x_{2, eq}/K_1) = 0$.

Below, a sample solution is shown for $k_1 = 1$ and $k_{-1} = 2$. These simulation results can be examined further, and they reveal three important observations; 1) the existence of a conservation quantity, 2) a thermodynamic driving force, and 3) the pooling of variables based on chemistry and thermodynamics.

```python
# Create MassModel
model = MassModel('Linear_Reversible')
# Generate the MassMetabolites
x1 = MassMetabolite("x1")
x2 = MassMetabolite("x2")
# Generate the MassReactions
v1 = MassReaction("v1")
# Add metabolites to the reaction, add reaction to the model
v1.add_metabolites({x1: -1, x2: 1})
model.add_reactions([v1])
# Set parameters and initial conditions
v1.kf = 1
v1.kr = 2
model.update_initial_conditions({x1: 1, x2: 0})
# Utilize type 2 rate law for kf and kr parameters defined
model.get_rate_expressions(rate_type=2, update_reactions=True)
model
```

    Set parameter Username

        Name	Linear_Reversible

        Memory address	0x07fc040b78850

        Stoichiometric Matrix
        2x1

        Matrix Rank
        1

        Number of metabolites
        2

        Initial conditions defined
        2/2

        Number of reactions
        1

        Number of genes
        0

        Number of enzyme modules
        0

        Number of groups
        0

        Objective expression
        0

        Compartments

```python
t0 = 0
tf = 2
sim = Simulation(model, verbose=True)
conc_sol, flux_sol = sim.simulate(model, time=(t0, tf), verbose=True)

# Define pools
pools = ["x1 - x2 / Keq_v1", "x1 + x2"]

for i, equation_str in enumerate(pools):
    pool_id = "p" + str(i + 1)
    conc_sol.make_aggregate_solution(
        pool_id, equation=equation_str,
        parameters={v1.Keq_str: v1.kf/v1.kr}, update=True)
```

    WARNING: No compartments found in model. Therefore creating compartment 'compartment' for entire model.

    Successfully loaded MassModel 'Linear_Reversible' into RoadRunner.
    Getting time points
    Setting output selections
    Setting simulation values for 'Linear_Reversible'
    Simulating 'Linear_Reversible'
    Simulation for 'Linear_Reversible' successful
    Adding 'Linear_Reversible' simulation solutions to output
    Updating stored solutions

```python
fig_4_1 = plt.figure(figsize=(9, 6))
gs = fig_4_1.add_gridspec(nrows=2, ncols=2, width_ratios=[1, 1.5],
                          height_ratios=[1, 1])

ax1 = fig_4_1.add_subplot(gs[0, 0])
ax2 = fig_4_1.add_subplot(gs[0, 1])
ax3 = fig_4_1.add_subplot(gs[1, 1])

plot_phase_portrait(
    conc_sol, x=x1, y=x2, ax=ax1,
    xlabel=x1.id, ylabel=x2.id,
    xlim=(-0.05, 1.05), ylim=(-0.05, 1.05),
    title=("(a) Phase portrait", {"size":"large"}),
    annotate_time_points="endpoints",
    annotate_time_points_labels=True);

plot_time_profile(
    conc_sol, ax=ax2, observable=model.metabolites, legend="right outside",
    xlabel="Time", ylabel="Concentrations",
    title=("(b) Time Profiles of Species", {"size": "large"}));

plot_time_profile(
    conc_sol, ax=ax3, observable=["p1", "p2"],
    legend="right outside",
    xlabel="Time", ylabel="Concentrations",
    title=("(c) Time Profiles of Pools", {"size": "large"}));
fig_4_1.tight_layout()
```

**Figure 4.1:** Dynamic simulation of the reaction $x_1 \underset{v_{-1}}{\stackrel{v_1}{\rightleftharpoons}} x_2$ for $k_1 =1$ and $k_{-1} = 2$, and $x_1(0)=1$, $x_2(0)=0$. (a) The phase portrait. (b) The time profiles. (c) The time profile of the pooled variables $p_1 = x_1 - x_2/K_1$ and $p_1 = x_1 + x_2$.

### Mass conservation:
The time profiles in Figure 4.1b show $x_1$ fall and $x_2$ rise to their equilibrium values. The phase portrait (Figure 4.1a) is a straight line of slope -1. This implies that

$$\begin{equation} p_1 = x_1 + x_2 = \big \langle (1, 1), (x_1, x_2)^T \big \rangle \tag{4.4} \end{equation}$$

is a constant. This summation represents a conservation quantity that stems from the fact that as $x_1$ reacts, $x_2$ appears in an equal and opposite amount. The stoichiometric matrix is singular with a rank of 1, showing that this is a one-dimensional dynamic system. It has a left null space that is spanned by the vector $(1, 1), i.e., (1, 1) \centerdot \textbf{S} = 0$, thus $p_2$ is in the left null space of $\textbf{S}$.

We also note that since $x_1 + x_2$ is a constant, we can describe the concentration of $x_1$ as a fraction of the total mass, i.e.,

$$\begin{equation*} f_1 = \frac{x_1}{x_1 + x_2} = \frac{x_1}{p_2} \end{equation*}$$

Pool sizes and the fraction of molecules in a particular state will be used later in the text to define physiologically useful quantities.

### Disequilibrium and the thermodynamic driving force:
A pooled variable:

$$\begin{equation} p_1 = x_1 - x_2/K_1 \tag{4.5} \end{equation}$$

can be formed (see Figure 4.1c). Combination of the differential equations for $x_1$ and $x_2$ leads to

$$\begin{equation} \frac{dp_1}{dt} = -(k_1 + k_{-1}) p_1 \tag{4.6} \end{equation}$$

and thus the time constant for this reaction is

$$\begin{equation} \tau_{1} = \frac{1}{k_1 + k_{-1}} \tag{4.7} \end{equation}$$

Note that when $t \rightarrow \infty, p_1 \rightarrow 0$ and then

$$\begin{equation} \frac{x_2}{x_1} \rightarrow \frac{k_1}{k_{-1}} = K_1 = \frac{x_{2,eq}}{x_{1, eq}} \tag{4.8} \end{equation}$$

the reaction has reached equilibrium. The pool $p_1$ thus represents a disequilibrium quantity  and represents the thermodynamic driver for the reaction, see Eq. (4.3). With an initial condition of $x_{1, 0} = 1$ and $K_1 = 1/2$, the eventual concentrations $(t \rightarrow \infty)$ will be $x_{1, eq} = 2/3$ and $x_{2, eq} = 1/3$.

### Representing dynamics with pools for reversible linear reactions
These considerations show that we can think about the dynamics of reaction (4.1) in terms of two pooled variables rather than the concentrations themselves. Thus, a useful pool transforming matrix for this reaction would be

$$\begin{equation} \textbf{P} = \begin{pmatrix} {1} & {-1/K_1} \\ {1}  & {1} \\ \end{pmatrix} \end{equation}$$
$$\tag{4.9}$$

leading to disequilibrium $(p_1)$ and conservation $(p_2)$ quantities associated with the reaction in Eq. (4.1). The former quantity moves on the time scale given by $\tau_1$ while the latter is time invariant. For practical purposes, the dynamics of the reaction have relaxed within a time duration of three to five times  $\tau_1$ (see Figure 4.1b).

The differential equations for the pools can be obtained as

$$\begin{equation} \textbf{P} \frac{d\textbf{x}}{dt} = \frac{d}{dt}\begin{pmatrix} {p_1} \\ {p_2} \\ \end{pmatrix} = -(k_1 + k_{-1}) \begin{pmatrix} {x_1 - x_2/K_1} \\ {0} \\ \end{pmatrix} = -(k_1 + k_{-1})\begin{pmatrix} {p_1} \\ {0} \\ \end{pmatrix} \end{equation}$$

Therefore, the conservation quantity is a constant (time derivative is zero) and the disequilibrium pool is driven by a thermodynamic driving force that is itself multiplied by $-(k_1 + k_{-1})$, that is the inverse of the time constant for the reaction. Thus, the three key features of chemical reactions, the stoichiometry, thermodynamics, and kinetics, are separately accounted for.

## The Reversible Bi-Linear Reaction
The reaction mechanism for the reversible bi-linear reaction is:

$$\begin{equation} x_1 + x_2 \underset{v_{-1}}{\stackrel{v_1}{\rightleftharpoons}} x_3 \tag{4.10} \end{equation}$$

where the elementary reaction rates are

$$\begin{equation} v_1 = k_1x_1x_2, \ v_{-1} = k_{-1}x_3 \tag{4.11} \end{equation}$$

The forward rate, $v_1$, is a non-linear function, or more specifically, a bi-linear function. The variable

$$\begin{equation} p_1 = x_1x_2 - x_3/K_1 \tag{4.12} \end{equation}$$

represents a disequilibrium quantity. The dynamic states of this system can be computed from

$$\begin{equation} \frac{dx_1}{dt} = -v_1 + v_{-1} = -k_1x_1x_2 + k_{-1}x_3 = -k_1(x_1x_2 - x_3/K_1) = \frac{dx_2}{dt} = -\frac{dx_3}{dt} \tag{4.13} \end{equation}$$

This example will be used to illustrate the essential features of a bi-linear reaction; 1) That there are two conservation quantities associated with it, 2) How to compute the equilibrium state, 3) The use of linearization and deviation variables from the equilibrium state, 4) The derivation of a single linear disequilibrium quantity, and 5) Formation of pools.

### Conservation quantities for reversible bi-linear reactions
The stoichiometric matrix is

$$\begin{equation} S = \begin{pmatrix} {-1} & {1} \\ {-1} & {1} \\ {1}  & {-1} \\ \end{pmatrix} \end{equation}$$

The stoichiometric matrix has a rank of 1, and thus the dynamic dimension of this system is 1. Two vectors that span the left null space of **S** are (1,0,1) and (0,1,1) and the corresponding conservation quantities are:

$$\begin{equation} p_2 = x_1 + x_3, \ p_3 = x_2 + x_3 \tag{4.14} \end{equation}$$

This selection of conservation quantities is not unique, as one can find other sets of two vectors that span the left null space.

### The equilibrium state for reversible bi-linear reactions
We can examine the equilibrium state for the specific parameter values to be used for numerical simulation below, Figure 4.2. At equilibrium, $p_1 \rightarrow 0$ and we have that $(K_1 = 1)$

$$\begin{equation} x_{1, eq}x_{2, eq} = x_{3, eq} \tag{4.15} \end{equation}$$

and that

$$\begin{equation} x_1(0) = 3 =  x_{1, eq} + x_{3, eq}, \ \ \  x_2(0) = 2 =  x_{2, eq} + x_{3, eq} \tag{4.16} \end{equation}$$

These three equations can be combined to give a second order algebraic equation

$$\begin{equation} x_{3, eq}^2 - 6x_{3, eq} + 6 = 0  \tag{4.17} \end{equation}$$

that has a positive root that yields

$$\begin{equation} x_{1, eq} = 1.73,\ \  x_{2, eq} = 0.73, \ \ x_{3, eq} = 1.27 \tag{4.18} \end{equation}$$

### Linearization and deviation variables for reversible bi-linear reactions
Equation (13) can be linearized around the equilibrium point $\textbf{x}_{eq}$==(1.73,0.73,1.27) to give

$$\begin{equation} \frac{dx_1}{dt} = x_1x_2 - x_3  \tag{4.19} \end{equation}$$
$$\begin{equation} \rightarrow \frac{dx_1}{dt} = 0.73(x_1 -1.73) + 1.73(x_2 - 0.73) - (x_3 - 1.27) \tag{4.20} \end{equation}$$

where a numerical value of $k_1$ used is unity.

### The disequilibrium and conservation quantities for reversible bi-linear reactions
Equation (20) can be written in terms of deviation variables from the equilibrium state, i.e.,

$$\begin{equation} x_i' x_i - x_{i, eq} \tag{4.21} \end{equation}$$

as

$$\begin{equation} \frac{dx_1'}{dt} = 0.73x_1' + 1.73x_2' - x_3' = p_1' \tag{4.22} \end{equation}$$

which simply is the linearized version of the disequilibrium quantity in Eq.(4.12), and we have that

$$\begin{equation} \frac{dx_2'}{dt} =\frac{dx_1'}{dt} \ \ and \ \ \frac{dx_3'}{dt} = -\frac{dx_1'}{dt} \tag{4.23} \end{equation}$$

### Representing dynamics with pools for reversible bi-linear reactions
We can therefore form a pool transformation matrix as:

$$\begin{equation} \textbf{P} = \begin{pmatrix} {0.73} & {1.73} & {-1} \\ {1} & {0} & {1}  \\ {0} & {1} & {1} \\ \end{pmatrix} \end{equation}$$
$$\tag{4.24}$$

where the first pool represents the disequilibrium quantity and the second and third are conservation quantities. Now we transform the deviation variables with this matrix, i.e., $\textbf{p'} = \textbf{Px'}$ and can look at the time derivatives of the pools

$$\begin{align} \frac{d\textbf{p'}}{dt} &= \textbf{P}\frac{d\textbf{x'}}{dt} \\ &= \begin{pmatrix} {-3.46} & {3.46} \\ {0} & {0} \\ {0} & {0} \\ \end{pmatrix}(0.73x_1' + 1.73x_2' - x_3') \\ &= 3.46\begin{pmatrix} {1} \\ {0} \\ {0} \\ \end{pmatrix} p_1' \end{align}$$
$$\tag{4.25}$$

This result is similar to that obtained above for the linear reversible reaction. There are two conservation pools and a disequilibrium pool that is moved by itself multiplied by a characteristic rate constant. We note that the conservation quantities, for both the linear and bi-linear reaction, do not change if the reactions are irreversible (i.e., if $K_{eq} \rightarrow \infty$)

### Numerical simulation of reversible bi-linear reactions
The dynamic response of this reaction can readily be computed and the results graphed; see Figure 4.2.

```python
# Create MassModel
model = MassModel('BiLinear_Reversible')
# Generate the MassMetabolites
x1 = MassMetabolite("x1")
x2 = MassMetabolite("x2")
x3 = MassMetabolite("x3")
# Generate the MassReactions
v1 = MassReaction("v1")
# Add metabolites to the reaction, add reaction to the model
v1.add_metabolites({x1: -1, x2: -1, x3: 1})
model.add_reactions([v1])
# Set parameters and initial conditions
v1.kf = 1
v1.kr = 1
model.update_initial_conditions({x1: 3, x2: 2, x3: 0})
# Utilize type 2 rate law for kf and kr parameters defined
model.get_rate_expressions(rate_type=2, update_reactions=True)
model
```

        Name	BiLinear_Reversible

        Memory address	0x07fc040be38e0

        Stoichiometric Matrix
        3x1

        Matrix Rank
        1

        Number of metabolites
        3

        Initial conditions defined
        3/3

        Number of reactions
        1

        Number of genes
        0

        Number of enzyme modules
        0

        Number of groups
        0

        Objective expression
        0

        Compartments

```python
t0 = 0
tf = 5
sim = Simulation(model, verbose=True)
conc_sol, flux_sol = sim.simulate(model, time=(t0, tf), verbose=True)

# Define pools
pools = ['x1*x2 - x3 / Keq_v1', 'x1 + x3', 'x2 + x3']

for i, equation_str in enumerate(pools):
    pool_id = "p" + str(i + 1)
    conc_sol.make_aggregate_solution(
        pool_id, equation=equation_str,
        parameters={v1.Keq_str: v1.kf/v1.kr}, update=True)
```

    WARNING: No compartments found in model. Therefore creating compartment 'compartment' for entire model.

    Successfully loaded MassModel 'BiLinear_Reversible' into RoadRunner.
    Getting time points
    Setting output selections
    Setting simulation values for 'BiLinear_Reversible'
    Simulating 'BiLinear_Reversible'
    Simulation for 'BiLinear_Reversible' successful
    Adding 'BiLinear_Reversible' simulation solutions to output
    Updating stored solutions

```python
fig_4_2 = plt.figure(figsize=(12, 4))
gs = fig_4_2.add_gridspec(nrows=1, ncols=2)

ax1 = fig_4_2.add_subplot(gs[0, 0])
ax2 = fig_4_2.add_subplot(gs[0, 1])

plot_time_profile(
    conc_sol, ax=ax1, observable=model.metabolites,
    legend="lower outside",
    xlabel="Time", ylabel="Concentrations",
    title=("(a) Time Profiles of Species", {"size": "large"}));

plot_time_profile(
    conc_sol, ax=ax2, observable=["p1", "p2", "p3"],
    legend="lower outside",
    xlabel="Time", ylabel="Concentrations",
    title=("(b) Time Profiles of Pools", {"size": "large"}));
fig_4_2.tight_layout()
```

**Figure 4.2:** The concentration time profiles for the reaction $x_1 + x_2 {\rightleftharpoons} x_3$ for $k_1$ = $k_{-1} = 1$ and $x_1(0)=3$, $x_2(0)=2$, and $x_3(0)=0$. (a) The concentrations as a function of time. (b) The pools as a function of time.

## Connected Reversible Linear Reactions
Now we consider more than one reaction working simultaneously. We will consider two reversible first order reactions that are connected by an irreversible reaction;

$$\begin{equation} x_1 \underset{v_{-1}}{\stackrel{v_1}{\rightleftharpoons}} x_2 \stackrel{v_2} \rightarrow x_3 \underset{v_{-3}}{\stackrel{v_3}{\rightleftharpoons}} x_4 \tag{4.26} \end{equation}$$

The stoichiometric matrix and the reaction vector, are

$$\begin{equation} \textbf{S} = \begin{pmatrix} {-1} & {1} & {0} & {0} & {0}  \\ {1} & {-1} & {-1} & {0} & {0}  \\ {0} & {0} & {1} & {-1} & {1}  \\  {0} & {0} & {0} & {1} & {-1}  \\ \end{pmatrix}, \  \textbf{v}(\textbf{x})  = \begin{pmatrix}  {k_1x_1} \\  {k_{-1}x_2} \\ {k_2x_2} \\ {k_3x_3} \\ {k_{-3}x_4} \\\end{pmatrix}\end{equation}$$
$$\tag{4.27}$$

and thus the dynamic mass balances are;

$$\begin{align} \frac{dx_1}{dt} &= -k_1x_1 + k_{-1}x_2\ \ &\tag{4.28a} \\ \frac{dx_2}{dt} &=  k_1x_1 - k_{-1}x_2 - k_2x_2\ \ &\tag{4.28b} \\ \frac{dx_3}{dt} &=  k_2x_2 - k_3x_3 + k_{-3}x_4\ \ &\tag{4.28c} \\ \frac{dx_4}{dt} &=  k_3x_3 - k_{-3}x_4\ \ &\tag{4.28d} \\ \end {align}$$

The net reaction rates are:

$$\begin{equation} v_{1, net} = k_1x_1 - k_{-1}x_2 = k_1(x_1 - x_2/K_1)  \tag{4.29} \end{equation}$$

and

$$\begin{equation} v_{3, net} = k_3x_3 - k_{-3}x_4 = k_3(x_3 - x_4/K_3) \tag{4.30} \end{equation}$$

where $K_1 = k_1/k_{-1}$ and  $K_3 = k_3/k_{-3}$ are the equilibrium constants. This example can be used to illustrate three concepts: 1) dynamic decoupling, 2) stoichiometric decoupling, and 3) formation of multi-reaction pools.

### Dynamic decoupling through separated time scales:
This linear system can be described by

$$\begin{equation} \frac{d\textbf{x}}{dt} = \textbf{Jx} \tag{4.31} \end{equation}$$

where the Jacobian matrix for this system is obtained directly from the equations in (4.28):

$$\begin{equation} \textbf{J} = \begin{pmatrix} {-k_1} & {k_{-1}} & {0} & {0} \\ {k_1} & {-k_{-1} - k_2} & {0} & {0} \\ {0} & {k_2} & {-k_3} & {-k_{-3}} \\ {0} & {0} & {k_3} & {k_{-3}} \\ \end{pmatrix} \end{equation}$$
$$\tag{4.32}$$

Note that for linear systems, $\textbf{x} = \textbf{x'}$. Observe that the second column in $\textbf{J}$ is a combination of the second and third column in $\textbf{S}$;

$$\begin{equation} \begin{pmatrix} {j_{12}} \\ {j_{22}} \\ {j_{32}} \\ {j_{42}} \end{pmatrix} = \begin{pmatrix} {1} \\ {-1} \\ {0} \\ {0} \end{pmatrix} + \begin{pmatrix} {0} \\ {-1} \\ {1} \\ {0} \end{pmatrix} k_2 = k_{-1}\textbf{s}_2 + k_2\textbf{s}_3 \end{equation}$$
$$\tag{4.33}$$

```python
# Create MassModel
model = MassModel('Connected_Linear_Reversible')
# Generate the MassMetabolites
x1 = MassMetabolite("x1")
x2 = MassMetabolite("x2")
x3 = MassMetabolite("x3")
x4 = MassMetabolite("x4")
# Generate the MassReactions
v1 = MassReaction("v1")
v2 = MassReaction("v2", reversible=False)
v3 = MassReaction("v3")
# Add metabolites to the reaction, add reaction to the model
v1.add_metabolites({x1: -1, x2: 1})
v2.add_metabolites({x2: -1, x3: 1})
v3.add_metabolites({x3: -1, x4: 1})
model.add_reactions([v1, v2, v3])
# Set parameters and initial conditions
v1.kf = 1
v1.Keq = 1
v2.kf = 1
v3.kf = 1
v3.Keq = 1
model.update_initial_conditions({x1: 1, x2: 0, x3: 0, x4: 0})
model
```

        Name	Connected_Linear_Reversible

        Memory address	0x07fc0b04ceeb0

        Stoichiometric Matrix
        4x3

        Matrix Rank
        3

        Number of metabolites
        4

        Initial conditions defined
        4/4

        Number of reactions
        3

        Number of genes
        0

        Number of enzyme modules
        0

        Number of groups
        0

        Objective expression
        0

        Compartments

```python
t0 = 0
tf = 10

sim = Simulation(model, verbose=True)
conc_sol, flux_sol = sim.simulate(model, time=(t0, tf), verbose=True)
```

    WARNING: No compartments found in model. Therefore creating compartment 'compartment' for entire model.

    Successfully loaded MassModel 'Connected_Linear_Reversible' into RoadRunner.
    Getting time points
    Setting output selections
    Setting simulation values for 'Connected_Linear_Reversible'
    Simulating 'Connected_Linear_Reversible'
    Simulation for 'Connected_Linear_Reversible' successful
    Adding 'Connected_Linear_Reversible' simulation solutions to output
    Updating stored solutions

```python
fig_4_3 = plt.figure(figsize=(6, 4))
gs = fig_4_3.add_gridspec(nrows=1, ncols=1)

ax1 = fig_4_3.add_subplot(gs[0, 0])

plot_time_profile(
    conc_sol, ax=ax1, legend="right outside",
    xlabel="Time", ylabel="Concentrations",
    title=("Time Profiles of Species", {"size": "large"}));
fig_4_3.tight_layout()
```

**Figure 4.3:** The dynamic response of the reactions in Eq. (4.17) for $K_1=K_3=1$ and $k_1=k_2=k_3=1$. The graphs show the concentrations varying with time for $x_1(0)=1, x_2(0)=x_3(0)=x_4(0)=0$.

The kinetic effects of $x_2$ are thus felt through both reactions 2 and 3 (i.e., the second and third column in $\textbf{S}$ that are the corresponding reaction vectors), $\textbf{s}_2$ and $\textbf{s}_3$. These two reaction vectors are weighted by the rate constants (reciprocal of the time constants). Therefore, we expect that if $k_2$ is numerically much smaller than $k_{-1}$ then the dynamic coupling is 'weak.' We consider two sets of parameter values.

* First, we simulate this system with all the rate constants being equal, Figure 4.3. All the concentrations are moving on the same time scale. For a series of reactions, the overall dynamics are expected to unfold on a time scale that is the sum of the individual time constants. Here, this sum is three, and the dynamics have relaxed after a time period of three to five times this value.

* Next, we make the second reaction ten times slower compared to the other two by decreasing $k_2$. We see that the two faster reactions come to a quasi-equilibrium state relatively quickly (relative to reaction 3), and form two conservation pools that exchange mass slowly. The sum of the rate constants for the three reactions in series is now seven, and the dynamics unfold on this time scale.

#### Stoichiometric decoupling
Reaction 3 does not influence reaction 1 at all. They are separated by the irreversible reaction 2. Thus, changes in the kinetics of reaction 3 will not influence the progress of reaction 1. This can be illustrated through simulation by changing the rate constants for reaction 3 and observing what happens to reaction 1.

#### Formation of multi-reaction pools:
We can form the following pooled variables based on the properties of the individual reversible reactions

$$\begin{align} &p_1 = x_1 - x_2/K_1\ && disequilibrium\ quantity\ for\ reaction\ 1\ \ &\tag{4.34a} \\ &p_2 = x_1 + x_2    \ && conservation\ quantity\ for\ reaction\ 1 \ \ &\tag{4.34b} \\ &p_3 = x_3 - x_4/K_3\ && disequilibrium\ quantity\ for\ reaction\ 3 \ \ &\tag{4.34c} \\ &p_4 = x_3 + x_4    \ && conservation\ quantity\ for\ reaction\ 3\ \ &\tag{4.34d} \end{align}$$

A representation of the dynamics of this reaction system can be obtained by plotting these pools as a function of time; Figure 4.4a. To prepare this plot, we use the pooling matrix

$$\begin{equation} \textbf{P} = \begin{pmatrix} {1} & {-1/K_1} & {0} & {0} \\ {1} & {1} & {0} & {0} \\ {0} & {0} & {1} & {-1/K_3}  \\ {0} & {0} & {1} & {1} \\ \end{pmatrix} \end{equation}$$
$$\tag{4.35}$$

to post-process the output. However, we note that the conservation quantities associated with the individual reactions are no longer time-invariant.

The rank of $\textbf{S}$ is 3 and its one-dimensional left null space is spanned by (1,1,1,1); thus the conservation quantity is $x_1 + x_2 + x_3 + x_4$. Therefore an alternative pooling matrix may be formulated as

$$\begin{equation} \textbf{P} = \begin{pmatrix} {1} & {-1/K_1}  & {0} & {0} \\ {0} & {1} & {0} & {0} \\ {0} & {0} & {1} & {-1/K_3} \\ {1} & {1} & {1} & {1} \\ \end{pmatrix}\end{equation}$$
$$\tag{4.36}$$

where we use $x_2$ as the coupling variable and the overall conservation pool instead of the conservation pools associated with the individual reactions. The two conservation pools are combined into one overall mass conservation pool.

We can now derive the dynamic mass balances on the pools as

$$\begin{align} \frac{d\textbf{p}}{dt} = \textbf{PSv} &= \begin{pmatrix} {-(k_1 + k_{-1})(x_1 - x_2/K_1) +\frac{k_2}{K_1}x_2} \\ {k_1(x_1 - x_2/K_1) - k_2x_2} \\ {-(k_3 + k_{-3})(x_3 - x_4/K_3) +k_2x_2} \\ {0}\end{pmatrix}  \\ &= \begin{pmatrix} {-(k_1 + k_{-1})p_1 +\frac{k_2}{K_1}x_2} \\ {k_1p_1 - k_2p_2} \\ {-(k_3 + k_{-3})p_3 +k_2p_2} \\{0}\end{pmatrix} \\ &= \begin{pmatrix} {-(k_1 + k_{-1})} \\ {k_1}  \\ {0} \\ {0} \end{pmatrix}p_1 + \begin{pmatrix} {\frac{k_2}{K_1}} \\ {-k_2} \\ {k_2} \\ {0} \end{pmatrix}p_2 + \begin{pmatrix} {0} \\ {0} \\ {-(k_3 + k_{-3})} \\ {0} \end{pmatrix}p_3 \end{align}$$
$$\tag{4.37}$$

This equation shows that $p_1$ and $p_3$ create fast motion compared to $p_2$ given the relative numerical values of the rate constants; $p_2$ creates a slow drift in this system for the numerical values used in Figure 4.4b.

```python
# Define pools
reg_pools = ['x1 - x2 / Keq_v1', 'x1 + x2',
             'x3 - x4 / Keq_v3', 'x3 + x4']

alt_pools = ['x1 - x2 / Keq_v1', 'x2',
             'x3 - x4 / Keq_v3', 'x1 + x2 + x3 + x4']

for prefix, pools in zip(["p", "alt_p"], [reg_pools, alt_pools]):
    for i, equation_str in enumerate(pools):
        pool_id = prefix + str(i + 1)
        conc_sol.make_aggregate_solution(
            pool_id, equation=equation_str,
            parameters={v1.Keq_str: v1.Keq, v3.Keq_str: v3.Keq},
            update=True)
```

```python
fig_4_4 = plt.figure(figsize=(12, 8))
gs = fig_4_4.add_gridspec(nrows=2, ncols=2)

ax1 = fig_4_4.add_subplot(gs[0, 0])
ax2 = fig_4_4.add_subplot(gs[1, 0])
ax3 = fig_4_4.add_subplot(gs[1, 1])

plot_time_profile(
    conc_sol, ax=ax1, observable=model.metabolites,
    legend="right outside", xlabel="Time", ylabel="Concentrations",
    title=("(a) Time Profiles of Species", {"size": "large"}));

plot_time_profile(
    conc_sol, observable=["p1", "p2", "p3"], ax=ax2,
    legend="lower outside", xlabel="Time", ylabel="Concentrations",
    title=("(b) Time Profiles of Pools", {"size": "large"}));

plot_time_profile(
    conc_sol, observable=["alt_p1", "alt_p2", "alt_p3"], ax=ax3,
    legend="lower outside", xlabel="Time", ylabel="Concentrations",
    title=("(c) Time Profiles of Alternate Pools",  {"size": "large"}));
fig_4_4.tight_layout()
```

**Figure 4.4:** (a) The time profiles (b) The conservation pools $p_2 = x_1 + x_2$ and $p_4 = x_3 + x_4$ and the disequilibrium pools $p_1 = x_1 - x_2/K_1$ and $p_3 = x_3 - x_4/K_3$ for the individual reactions. The disequilibrium pools move quickly towards a quasi-equilibrium state, while the conservation pools move more slowly. These pools are defined in Eq (4.35). (c) The dynamic response with alternative pools; $p_2 = x_2$, and $p_4 = x_1 + x_2 + x_3 + x_4$. These pools are defined in Eq (4.36).

## Connected Reversible Bi-linear Reactions
An important case of connected bi-linear reactions is represented by the reaction mechanism

$$\begin{equation} x_1 + x_2 \underset{v_{-1}}{\stackrel{v_1}{\rightleftharpoons}} x_3 \underset{v_{-2}}{\stackrel{v_2}{\rightleftharpoons}} x_4 + x_5 \tag{4.38} \end{equation}$$

This reaction network is similar to reaction mechanisms for enzymes, and thus leads us into the treatment of enzyme kinetics (Chapter 5). The elementary reaction rates are:

$$\begin{equation} v_1 = k_1x_1x_2, \ \  v_{-1} = k_{-1}x_3 \ \ v_2 = k_2x_3, \text{and} \ \ v_{-2} = k_{-2}x_4x_5 \tag{4.39} \end{equation}$$

and the equilibrium constants are $K_1 = k_1/k_{-1}$ and $K_2 = k_2/k_{-2}$. There are two disequilibrium quantities.

$$\begin{equation} p_1 = x_1x_2 - x_3 / K_1 \tag{4.40} \end{equation}$$
$$\begin{equation} p_2 = x_3 - x_4x_5 / K_2 \tag{4.41} \end{equation}$$

We now explore the same features of this coupled system of bi-linear reactions as we did for the single reversible bi-linear reaction.

### Conservation quantities for connected reversible bi-linear reactions
The (5x4) stoichiometric matrix

$$\begin{equation} \textbf{S} = \begin{pmatrix} {-1} & {1} & {0} & {0} \\ {-1} & {1} & {0} & {0} \\ {1} & {-1} & {-1} & {1} \\ {0} & {0} & {1} & {-1} \\ {0} & {0} & {1} & {-1} \\ \end{pmatrix} \end{equation}$$
$$\tag{4.42}$$

has a rank of 2 and thus there are three conservation variables and two independent dynamic variables.

The conservation quantities are not unique and which one we will use depends on the reaction chemistry that is being studied. An example is

$$\begin{equation} AB + C \underset{v_{-1}}{\stackrel{v_1}{\rightleftharpoons}} ABC \underset{v_{-2}}{\stackrel{v_2}{\rightleftharpoons}} A + BC \tag{4.43} \end{equation}$$

in which case the three independent conservation quantities would be:

$$\begin{align} \text{Conservation of A}:\ p_3 &=  x_1 + x_3 + x_4\ \ &\tag{4.44a} \\ \text{Conservation of B}:\ p_4 &=  x_1 + x_3 + x_5\ \ &\tag{4.44b} \\ \text{Conservation of C}:\ p_5 &=  x_2 + x_3 + x_5\ \ &\tag{4.44c} \end{align}$$

These are convex quantities as all the coefficients are non-negative (the concentrations are $x_i>0$). The individual bi-linear reactions have two each, but once coupled, the number of conservation quantities drops by one.

### The equilibrium state for connected reversible bi-linear reactions
The computation of the equilibrium state involves setting the net fluxes to zero and combining those equations with the conservation quantities to get a set of independent equations. For convenience of illustration, we pick $K_1 = K_2 = 1$ and the equilibrium equations become

$$\begin{equation} x_{1, eq}x_{2, eq} = x_{3, eq} = x_{4, eq}x_{5, eq} \tag{4.45} \end{equation}$$

and if we pick $p_3 = p_4 = p_5 = 3$ then the solution for the equilibrium state is simple: $x_{1, eq}x_{2, eq} = x_{3, eq} = x_{4, eq}x_{5, eq} = 1$. These equations can also be solved for arbitrary parameter values.

### Linearization and deviation variables for connected reversible bi-linear reactions
By linearizing the differential equations around the steady state we obtain

$$\begin{align} \frac{dx_1'}{dt} &= \frac{dx_2'}{dt} = -(k_1x_{1,eq})x_2' -(k_1x_{2,eq})x_1' + k_{-1}x_3' \\ \frac{dx_3'}{dt} &= (k_1x_{2,eq})x_1' + (k_1x_{1,eq})x_2' - (k_{-1} + k_2)x_3' + (k_{-2}x_{5,eq})x_4'+ (k_{-2}x_{4,eq})x_5' \\ \frac{dx_4'}{dt} &= \frac{dx_5'}{dt} = k_{2}x_3' - (k_{-2}x_{5,eq})x_4' -(k_{-2}x_{4,eq})x_5' \end{align}$$
$$\tag{4.46}$$

where $x_i' = x_i - x_{i, eq}$ represent the concentration deviation around equilibrium, $i$ =1,2,3,4 and 5.

### The disequilibrium and conservation quantities for connected reversible bi-linear reactions
Similar to the reversible bi-linear reaction, we obtain two pools that represent the disequilibrium driving forces of the two reactions,

$$\begin{align} p_1 &= x_1x_2 - x_3 / K_1 \approx (x_{2, eq})x_1' + (x_{1, eq})x_2' - (1/K_1)x_3' = p_1'\ \ &\tag{4.47a} \\ p_2 &= x_3 - x_4x_5 / K_2 \approx x_3' - (x_{5, eq}/K_2)x_4' - (x_{4, eq}/K_2)x_5' = p_2'\ \ &\tag{4.47b}  \end{align}$$

and the three pools that represent conservative quantities do not change:

$$\begin{align} p_3 &=  x_1 + x_3 + x_4\ \ &\tag{4.48a} \\ p_4 &=  x_1 + x_3 + x_5\ \ &\tag{4.48b} \\ p_5 &=  x_2 + x_3 + x_5\ \ &\tag{4.48c} \end{align}$$

We thus can define the pooling matrix as:

$$\begin{align} \textbf{P} &= \begin{pmatrix} {x_{2, eq}} & {x_{1, eq}} & {1/K_1} & {0} & {0} \\ {0} & {0} & {1} & {-x_{5, eq}/K_2} & {-x_{4, eq}/K_2} \\ {1} & {0} & {1} & {1} & {0} \\ {1} & {0} & {1} & {0} & {1} \\ {0} & {1} & {1} & {0} & {1} \\ \end{pmatrix} \\ &= \begin{pmatrix}  {1} & {1} & {-1} & {0} & {0} \\ {0} & {0} & {1} & {-1} & {-1} \\ {1} & {0} & {1} & {1} & {0} \\ {1} & {0} & {1} & {0} & {1} \\ {0} & {1} & {1} & {0} & {1} \\ \end{pmatrix} \end{align}$$
$$\tag{4.49}$$

for the particular equilibrium constants and concentrations values given above.

The differential equations for the pools are then formed by (and at this stage we remove the conservation pools as they are always constant):

$$\begin{equation} \frac{d\textbf{p'}}{dt} = \textbf{PSv(x)} \ \text{where} \ \textbf{v(x)} \approx \begin{pmatrix} {k_1p_1'} \\ {k_2p_2'} \\ \end{pmatrix} = \begin{pmatrix} {k_1} & {0} \\ {0} & {k_2} \\ \end{pmatrix} \begin{pmatrix} {p_1'} \\ {p_2'} \\ \end{pmatrix} \end{equation}$$
$$\tag{4.50}$$

which gives

$$\begin{equation} \frac{d\textbf{p'}}{dt} = \begin{pmatrix} {-(x_{2, eq} + x_{1, eq} + 1/K_1)} \\ {1} \\ \end{pmatrix}k_1p_1' + \begin{pmatrix} {1/K_1} \\ {-(1 + (x_{5, eq} + x_{4, eq})/K_2} \\ \end{pmatrix}k_2p_2' \end{equation}$$
$$\tag{4.51}$$

### Numerical simulation of connected reversible bi-linear reactions
These equations can be simulated once parameter values and initial conditions are specified. In order to illustrate the dynamic behavior in terms of the pools, we consider the particular situation where $K_1 = K_2 = x_{1, eq} = x_{2, eq} = x_{3, eq} = x_{4, eq} = x_{5, eq} = 1$, see Figure 4.5.

```python
# Create MassModel
model = MassModel('Connected_BiLinear_Reversible')
# Generate the MassMetabolites
x1 = MassMetabolite("x1")
x2 = MassMetabolite("x2")
x3 = MassMetabolite("x3")
x4 = MassMetabolite("x4")
x5 = MassMetabolite("x5")
# Generate the MassReactions
v1 = MassReaction("v1")
v2 = MassReaction("v2")
# Add metabolites to the reaction, add reaction to the model
v1.add_metabolites({x1: -1, x2: -1, x3: 1})
v2.add_metabolites({x3: -1, x4: 1, x5: 1})
model.add_reactions([v1, v2])
# Set parameters and initial conditions
v1.kf = 1
v1.kr = 1
v2.kf = 1
v2.kr = 1
model.update_initial_conditions({x1: 3, x2: 3, x3: 0, x4: 0, x5: 0})

# Utilize type 2 rate law for kf and kr parameters defined
model.get_rate_expressions(rate_type=2, update_reactions=True)
model
```

        Name	Connected_BiLinear_Reversible

        Memory address	0x07fc0f0571580

        Stoichiometric Matrix
        5x2

        Matrix Rank
        2

        Number of metabolites
        5

        Initial conditions defined
        5/5

        Number of reactions
        2

        Number of genes
        0

        Number of enzyme modules
        0

        Number of groups
        0

        Objective expression
        0

        Compartments

```python
t0 = 0
tf = 10
sim = Simulation(model, verbose=True)
conc_sol, flux_sol = sim.simulate(model, time=(t0, tf), verbose=True)

# Define pools
pools = ['x1*x2 - x3 / Keq_v1', 'x3 - x4*x5 / Keq_v2',
         'x1 + x3 + x4', 'x1 + x3 + x5', 'x2 + x3 + x4']

for i, equation_str in enumerate(pools):
    pool_id = "p" + str(i + 1)
    conc_sol.make_aggregate_solution(
        pool_id, equation=equation_str,
        parameters={v1.Keq_str: v1.kf/v1.kr,
                    v2.Keq_str: v2.kf/v2.kr}, update=True)
```

    WARNING: No compartments found in model. Therefore creating compartment 'compartment' for entire model.

    Successfully loaded MassModel 'Connected_BiLinear_Reversible' into RoadRunner.
    Getting time points
    Setting output selections
    Setting simulation values for 'Connected_BiLinear_Reversible'
    Simulating 'Connected_BiLinear_Reversible'
    Simulation for 'Connected_BiLinear_Reversible' successful
    Adding 'Connected_BiLinear_Reversible' simulation solutions to output
    Updating stored solutions

```python
fig_4_5 = plt.figure(figsize=(12, 4))
gs = fig_4_5.add_gridspec(nrows=1, ncols=2)

ax1 = fig_4_5.add_subplot(gs[0, 0])
ax2 = fig_4_5.add_subplot(gs[0, 1])

plot_time_profile(
    conc_sol, observable=model.metabolites, ax=ax1, legend="left outside",
    xlabel="Time", ylabel="Concentrations",
    title=("(a) Time Profiles of Species",  {"size": "large"}));

plot_time_profile(
    conc_sol, ax=ax2, observable=["p1", "p2", "p3", "p4", "p5"],
    legend="right outside",
    xlabel="Time", ylabel="Concentrations",
    title=("(a) Time Profiles of Pools",  {"size": "large"}));
fig_4_5.tight_layout()
```

**Figure 4.5:** The concentration time profiles for the reaction system $x_1 + x_2 \rightleftharpoons x_3 \rightleftharpoons x_4 + x_5$ for $k_1 = k_{-1} = k_2 = k_{-2} = 1$
and $x_1(0)=3, x_2(0)=3, x_3(0)=0, x_4(0)=0, x_5(0)=0$ (a) The concentrations as a function of time. (b) The pools as a function of time.

In this situation, the dynamic equation for the linearized pools becomes

$$\begin{equation} \frac{d\textbf{p'}}{dt} = \begin{pmatrix} {-3} \\ {1} \\ \end{pmatrix}k_1p_1' + \begin{pmatrix} {1} \\ {-3} \\ \end{pmatrix}k_2p_2' \end{equation}$$
$$\tag{4.52}$$

We can solve this equation and present the results with a dynamic phase portrait, Figure 4.6. The dynamic behavior of the non-equilibrium pools is shown for a range of parameters. We make three observations here.

1. Figure 4.6 shows that the dynamics for the pools can be decomposed to consider a fast equilibration of the two disequilibrium pools followed by the slow decay of the slower disequilibrium pool (Change values for $k_1$ and $k_2$ to visualize).

2. When reaction 1 is 10 times faster than reaction 2, then initial motion is along the vector $(-3,1)^T$, and when reaction 1 is 10 times slower than reaction 2, then initial motion is along the vector $(1,-3)^T$,

3. The linearized pools move in a similar fashion to the bi-linear disequilibrium pools, see Figure 4.6. The bi-linear and linear simulation do not change that much even though $x_1$, $x_2$, $x_4$, $x_5$ are 25% from their equilibrium value, and $x_3$ is 50% away from equilibrium.

```python
# Set new initial conditions
model.update_initial_conditions({x1: 0.75, x2: 0.75, x3: 1.5,
                                 x4: 0.75, x5: 0.75})
sim.update_model_simulation_values(model)
conc_sol, flux_sol = sim.simulate(model, time=(t0, tf))

for i, equation_str in enumerate(pools):
    pool_id = "p" + str(i + 1)
    conc_sol.make_aggregate_solution(
        pool_id, equation=equation_str,
        parameters={v1.Keq_str: v1.kf/v1.kr,
                    v2.Keq_str: v2.kf/v2.kr}, update=True)
```

```python
# Visualize solution
fig_4_6 = plt.figure(figsize=(5, 5))
gs = fig_4_6.add_gridspec(nrows=1, ncols=1)

ax1 = fig_4_6.add_subplot(gs[0, 0])

plot_phase_portrait(
    conc_sol, x="p1", y="p2", ax=ax1, legend="best",
    xlabel="p1", ylabel="p2", xlim=(-1.5, .5), ylim=(-.5, 1.5),
    title=("Phase portrait of p1 vs p2", {"size": "large"}),
    annotate_time_points="endpoints",
    annotate_time_points_labels=True);
fig_4_6.tight_layout()
```

**Figure 4.6:** The dynamic response of $x_1 + x_2 \rightleftharpoons x_3 \rightleftharpoons x_4 + x_5$ for $K_1 = K_{-1} = 1$. The graphs show the concentrations varying with time for $x_3(0)=1.5, \ x_1(0) = x_2(0) = x_4(0) = x_5(0) =0.75$ The disequilibrium pools $p_1 = x_1x_2 - x_3 / K_1$ (x-axis) and $p_2 = x_3 - x_4x_5 / K_2$ (y-axis) shown in a phase portrait.

## Summary

* Chemical properties associated with chemical reactions are; stoichiometry thermodynamics, and kinetics. The first two are physico-chemical properties, while the third can be biologically altered through enzyme action.

* Each net reaction can be described by pooled variables that represent a dis-equilibrium quantity and a mass conservation quantity that is associated with the reaction.

* If a reaction is fast compared to its network environment, its disequilibrium variable can be relaxed and then described by the conservation quantity associated with the reaction.

* Linearizing bi-linear rate laws does not create much error for small changes around the reference state.

* Removing a time scale from a model corresponds to reducing the dynamic dimension of the transient response by one.

* As the number of reactions grow, the number of conservation quantities may change.

* Irreversibility of reactions does not change the number of conservation quantities for a system.

$\tiny{\text{© B. Ø. Palsson 2011;}\ \text{This publication is in copyright.}\\ \text{Subject to statutory exception and to the provisions of relevant collective licensing agreements,}\\ \text{no reproduction of any part may take place without the written permission of Cambridge University Press.}}$


## SurfinPy

#### Tutorial 5 - Chemical Potential vs Chemical Potential Including Vibration Properties

In the Tutorial 2 we showed how experimental data could be used to determine the temperature dependent free energy term for gaseous species and then plot a phase diagram that represents 298 K.  In this example we will expand this methodology to calculate the vibrational properties for solid phases (<i>i.e.</i> zero point energy, vibrational entropy) and include these values in the generation of the phase diagrams.  This allows for a more accurate calculation of phase diagrams without the need to include experimental corrections for solid phases.

##### Methodoloy

As with tutorial 2 for gaseous species, the standard free energy varies significantly with temperature, and as DFT simulations are designed for condensed phase systems, we use experimental data to determine the temperature dependent free energy term for gaseous species obtained from the NIST database.  In addition we also calculate the vibrational properties for the solid phases modifying the free energy (G) for solid phases to be;

\begin{align}
\Delta G_f = U_0 + U_{\text{ZPE}} +A_{\text{vib}}
\end{align}

$U_0$ is the calculated internal energy from a DFT calculation, $U_{\text{ZPE}}$ is the zero point energy and $S_{\text{vib}}$ is the vibrational entropy.

\begin{align}
	U_{\text{ZPE}} = \sum_i^{3n} \frac{R \theta_i}{2}
\end{align}

where $A_{\text{vib}}$ is the vibrational Helmholtz free energy and defined as;

\begin{align}
	A_{\text{vib}} = \sum_i^{3n} RT \ln{(1-e^{-\theta_i/T})}
\end{align}

3n is the total number of vibrational modes, n is the number of species and $\theta_i$ is the characteristic vibrational temperature (frequency of the vibrational mode in Kelvin).

\begin{equation}
	\theta_i = \frac{h\nu_i}{k_B}
\end{equation}

##### code

```python
import matplotlib.pyplot as plt
from surfinpy import bulk_mu_vs_mu as bmvm
from surfinpy import utils as ut
from surfinpy import data
```

```python
temperature_range = [298, 299]
```

SurfinPy has the function to calculate the zero point energy and the vibrational entropy when entropy and zpe = True.

```python
bulk = data.ReferenceDataSet(cation = 1, anion = 1, energy = -92.0, funits = 10, file = 'ref_files/bulk_vib.yaml', entropy=True, zpe=True, temp_range=temperature_range)
```

In addition to entropy and zpe keyword you must provide the a file containing the vibrational modes and number of formula units used in taht calculations.  You must create the yaml file using the following format

```
    F-Units : number
    Frequencies :
    - mode1
    - mode2

```

Vibrational modes can be calculated via a density functional pertibation calculation or via the phonopy code.

```python

Bulk = data.DataSet(cation = 10, x = 0, y = 0, energy = -92,
                   label = "Bulk", entropy = True, zpe=True, file = 'ref_files/bulk_vib.yaml',
                   funits = 10, temp_range=temperature_range)

A = data.DataSet(cation = 10, x = 5, y = 20, energy = -468,
                   label = "A", entropy = True, zpe=True, file = 'ref_files/A_vib.yaml',
                   funits = 5, temp_range=temperature_range)

B = data.DataSet(cation = 10, x = 0, y = 10, energy = -228,
                   label = "B", entropy = True, zpe=True, file = 'ref_files/B_vib.yaml',
                   funits =  10, temp_range=temperature_range)

C = data.DataSet(cation = 10, x = 10, y = 30, energy = -706,
                   label = "C", entropy = True, zpe=True, file = 'ref_files/C_vib.yaml',
                   funits = 10, temp_range=temperature_range)

D = data.DataSet(cation = 10, x = 10, y = 0, energy = -310,
                   label = "D", entropy = True, zpe=True,  file = 'ref_files/D_vib.yaml',
                   funits =  10, temp_range=temperature_range)

E = data.DataSet(cation = 10, x = 10, y = 50, energy = -972,
                   label = "E", entropy = True, zpe=True, file = 'ref_files/E_vib.yaml',
                   funits =  10, temp_range=temperature_range)

F = data.DataSet(cation = 10, x = 8, y = 10, energy = -398,
                   label = "F", entropy = True, zpe=True, file = 'ref_files/F_vib.yaml',
                   funits =  2, temp_range=temperature_range)
```

```python
data = [Bulk, A, B, C,  D, E, F]
```

```python
x_energy=-20.53412969
y_energy=-12.83725889
```

```python
CO2_exp = ut.fit_nist("ref_files/CO2.txt")[298]
Water_exp = ut.fit_nist("ref_files/H2O.txt")[298]

CO2_corrected = x_energy + CO2_exp
Water_corrected = y_energy + Water_exp

deltaX = {'Range': [ -1, 0.6],  'Label': 'CO_2'}
deltaY = {'Range': [ -1, 0.6], 'Label': 'H_2O'}
```

```python
temp_298 = bmvm.calculate(data, bulk, deltaX, deltaY, CO2_corrected, Water_corrected)
ax = temp_298.plot_mu_p(temperature=298, colourmap="RdBu", figsize=(6, 4.5))

```

```python

```


##### Copyright 2020 The TensorFlow Authors.

```python
#@title Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
```

# MNIST classification

    View on TensorFlow.org

    Run in Google Colab

    View source on GitHub

    Download notebook

This tutorial builds a quantum neural network (QNN) to classify a simplified version of MNIST, similar to the approach used in Farhi et al. The performance of the quantum neural network on this classical data problem is compared with a classical neural network.

## Setup

```python
!pip install -q tensorflow==2.3.1
```

Install TensorFlow Quantum:

```python
!pip install -q tensorflow-quantum
```

Now import TensorFlow and the module dependencies:

```python
import tensorflow as tf
import tensorflow_quantum as tfq

import cirq
import sympy
import numpy as np
import seaborn as sns
import collections

# visualization tools
%matplotlib inline
import matplotlib.pyplot as plt
from cirq.contrib.svg import SVGCircuit
```

## 1. Load the data

In this tutorial you will build a binary classifier to distinguish between the digits 3 and 6, following Farhi et al. This section covers the data handling that:

- Loads the raw data from Keras.
- Filters the dataset to only 3s and 6s.
- Downscales the images so they fit can fit in a quantum computer.
- Removes any contradictory examples.
- Converts the binary images to Cirq circuits.
- Converts the Cirq circuits to TensorFlow Quantum circuits.

### 1.1 Load the raw data

Load the MNIST dataset distributed with Keras.

```python
(x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data()

# Rescale the images from [0,255] to the [0.0,1.0] range.
x_train, x_test = x_train[..., np.newaxis]/255.0, x_test[..., np.newaxis]/255.0

print("Number of original training examples:", len(x_train))
print("Number of original test examples:", len(x_test))

print("x_train.shape: ", x_train.shape)
print("x_test.shape: ", x_test.shape)
```

Filter the dataset to keep just the 3s and 6s,  remove the other classes. At the same time convert the label, `y`, to boolean: `True` for `3` and `False` for 6.

```python
def filter_36(x, y):
    keep = (y == 3) | (y == 6)
    x, y = x[keep], y[keep]
    y = y == 3
    return x,y
```

```python
x_train, y_train = filter_36(x_train, y_train)
x_test, y_test = filter_36(x_test, y_test)

print("Number of filtered training examples:", len(x_train))
print("Number of filtered test examples:", len(x_test))
```

Show the first example:

```python
idx = 10001

print(y_train[idx])
# print(y_train)
print("y_train.shape: ", y_train.shape)

# print(x_train)
print(x_train.shape)
plt.imshow(x_train[idx, :, :, 0])
plt.colorbar()
```

### 1.2 Downscale the images

An image size of 28x28 is much too large for current quantum computers. Resize the image down to 4x4:

```python
x_train_small = tf.image.resize(x_train, (4,4)).numpy()
x_test_small = tf.image.resize(x_test, (4,4)).numpy()
```

Again, display the first training example—after resize:

```python
print(y_train[idx])

plt.imshow(x_train_small[idx,:,:,0], vmin=0, vmax=1)
plt.colorbar()
```

### 1.3 Remove contradictory examples

From section *3.3 Learning to Distinguish Digits* of Farhi et al., filter the dataset to remove images that are labeled as belonging to both classes.

This is not a standard machine-learning procedure, but is included in the interest of following the paper.

```python
def remove_contradicting(xs, ys):

    print("xs.shape: ", xs.shape)
    print("ys.shape: ", ys.shape)

    mapping = collections.defaultdict(set)
    # Determine the set of labels for each unique image:
    for x,y in zip(xs,ys):
       mapping[tuple(x.flatten())].add(y)
    print("mapping.items(): ", mapping.items())

    new_x = []
    new_y = []
    for x,y in zip(xs, ys):
      labels = mapping[tuple(x.flatten())]
      if len(labels) == 1:
          new_x.append(x)
          new_y.append(labels.pop())
      else:
          # Throw out images that match more than one label.
          pass

    num_3 = sum(1 for value in mapping.values() if True in value)
    num_6 = sum(1 for value in mapping.values() if False in value)
    num_both = sum(1 for value in mapping.values() if len(value) == 2)

    print("Number of unique images:", len(mapping.values()))
    print("Number of 3s: ", num_3)
    print("Number of 6s: ", num_6)
    print("Number of contradictory images: ", num_both)
    print()
    print("Initial number of examples: ", len(xs))
    print("Remaining non-contradictory examples: ", len(new_x))

    return np.array(new_x), np.array(new_y)
```

The resulting counts do not closely match the reported values, but the exact procedure is not specified.

It is also worth noting here that applying filtering contradictory examples at this point does not totally prevent the model from receiving contradictory training examples: the next step binarizes the data which will cause more collisions.

```python
x_train_nocon, y_train_nocon = remove_contradicting(x_train_small, y_train)
```

### 1.3 Encode the data as quantum circuits

To process images using a quantum computer, Farhi et al. proposed representing each pixel with a qubit, with the state depending on the value of the pixel. The first step is to convert to a binary encoding.

```python
THRESHOLD = 0.5
print(x_train_nocon.shape)
# print(x_train_nocon)
print(x_test_small.shape)
x_train_bin = np.array(x_train_nocon > THRESHOLD, dtype=np.float32)
x_test_bin = np.array(x_test_small > THRESHOLD, dtype=np.float32)
print(x_train_bin.shape)
# print(x_train_bin)
print(x_test_bin.shape)
```

If you were to remove contradictory images at this point you would be left with only 193, likely not enough for effective training.

```python
_ = remove_contradicting(x_train_bin, y_train_nocon)
```

The qubits at pixel indices with values that exceed a threshold, are rotated through an $X$ gate.

```python
def convert_to_circuit(image):
    """Encode truncated classical image into quantum datapoint."""
    values = np.ndarray.flatten(image)
    qubits = cirq.GridQubit.rect(4, 4)
    circuit = cirq.Circuit()
    for i, value in enumerate(values):
        if value:
            circuit.append(cirq.X(qubits[i]))
    return circuit

idx = 100
print(x_train_bin[idx])
x_train_circ = [convert_to_circuit(x) for x in x_train_bin]
x_test_circ = [convert_to_circuit(x) for x in x_test_bin]
print(x_train_circ[idx])
```

Here is the circuit created for the first example (circuit diagrams do not show qubits with zero gates):

```python
SVGCircuit(x_train_circ[idx])
```

Compare this circuit to the indices where the image value exceeds the threshold:

```python
bin_img = x_train_bin[idx,:,:,0]
indices = np.array(np.where(bin_img)).T
indices
```

Convert these `Cirq` circuits to tensors for `tfq`:

```python
x_train_tfcirc = tfq.convert_to_tensor(x_train_circ)
x_test_tfcirc = tfq.convert_to_tensor(x_test_circ)
```

**!!!finish till here!!!**

## 2. Quantum neural network

There is little guidance for a quantum circuit structure that classifies images. Since the classification is based on the expectation of the readout qubit, Farhi et al. propose using two qubit gates, with the readout qubit always acted upon. This is similar in some ways to running small a Unitary RNN across the pixels.

### 2.1 Build the model circuit

This following example shows this layered approach. Each layer uses *n* instances of the same gate, with each of the data qubits acting on the readout qubit.

Start with a simple class that will add a layer of these gates to a circuit:

```python
class CircuitLayerBuilder():
    def __init__(self, data_qubits, readout):
        self.data_qubits = data_qubits
        self.readout = readout

    def add_layer(self, circuit, gate, prefix):
        for i, qubit in enumerate(self.data_qubits):
            symbol = sympy.Symbol(prefix + '-' + str(i))
            circuit.append(gate(qubit, self.readout)**symbol)
```

Build an example circuit layer to see how it looks:

```python
demo_builder = CircuitLayerBuilder(data_qubits = cirq.GridQubit.rect(4,1),
                                   readout=cirq.GridQubit(-1,-1))

circuit = cirq.Circuit()
demo_builder.add_layer(circuit, gate = cirq.XX, prefix='xx')
SVGCircuit(circuit)
```

Now build a two-layered model, matching the data-circuit size, and include the preparation and readout operations.

```python
def create_quantum_model():
    """Create a QNN model circuit and readout operation to go along with it."""
    data_qubits = cirq.GridQubit.rect(4, 4)  # a 4x4 grid.
    readout = cirq.GridQubit(-1, -1)         # a single qubit at [-1,-1]
    circuit = cirq.Circuit()

    # Prepare the readout qubit.
    circuit.append(cirq.X(readout))
    circuit.append(cirq.H(readout))

    builder = CircuitLayerBuilder(
        data_qubits = data_qubits,
        readout=readout)

    # Then add layers (experiment by adding more).
    builder.add_layer(circuit, cirq.XX, "xx1")
    builder.add_layer(circuit, cirq.ZZ, "zz1")

    # Finally, prepare the readout qubit.
    circuit.append(cirq.H(readout))

    return circuit, cirq.Z(readout)
```

```python
model_circuit, model_readout = create_quantum_model()
```

### 2.2 Wrap the model-circuit in a tfq-keras model

Build the Keras model with the quantum components. This model is fed the "quantum data", from `x_train_circ`, that encodes the classical data. It uses a *Parametrized Quantum Circuit* layer, `tfq.layers.PQC`, to train the model circuit, on the quantum data.

To classify these images, Farhi et al. proposed taking the expectation of a readout qubit in a parameterized circuit. The expectation returns a value between 1 and -1.

```python
# Build the Keras model.
model = tf.keras.Sequential([
    # The input is the data-circuit, encoded as a tf.string
    tf.keras.layers.Input(shape=(), dtype=tf.string),
    # The PQC layer returns the expected value of the readout gate, range [-1,1].
    tfq.layers.PQC(model_circuit, model_readout),
])
```

Next, describe the training procedure to the model, using the `compile` method.

Since the the expected readout is in the range `[-1,1]`, optimizing the hinge loss is a somewhat natural fit.

Note: Another valid approach would be to shift the output range to `[0,1]`, and treat it as the probability the model assigns to class `3`. This could be used with a standard a `tf.losses.BinaryCrossentropy` loss.

To use the hinge loss here you need to make two small adjustments. First convert the labels, `y_train_nocon`, from boolean to `[-1,1]`, as expected by the hinge loss.

```python
y_train_hinge = 2.0*y_train_nocon-1.0
y_test_hinge = 2.0*y_test-1.0
```

Second, use a custiom `hinge_accuracy` metric that correctly handles `[-1, 1]` as the `y_true` labels argument.
`tf.losses.BinaryAccuracy(threshold=0.0)` expects `y_true` to be a boolean, and so can't be used with hinge loss).

```python
def hinge_accuracy(y_true, y_pred):
    y_true = tf.squeeze(y_true) > 0.0
    y_pred = tf.squeeze(y_pred) > 0.0
    result = tf.cast(y_true == y_pred, tf.float32)

    return tf.reduce_mean(result)
```

```python
model.compile(
    loss=tf.keras.losses.Hinge(),
    optimizer=tf.keras.optimizers.Adam(),
    metrics=[hinge_accuracy])
```

```python
print(model.summary())
```

### Train the quantum model

Now train the model—this takes about 45 min. If you don't want to wait that long, use a small subset of the data (set `NUM_EXAMPLES=500`, below). This doesn't really affect the model's progress during training (it only has 32 parameters, and doesn't need much data to constrain these). Using fewer examples just ends training earlier (5min), but runs long enough to show that it is making progress in the validation logs.

```python
EPOCHS = 3
BATCH_SIZE = 32

NUM_EXAMPLES = len(x_train_tfcirc)
```

```python
x_train_tfcirc_sub = x_train_tfcirc[:NUM_EXAMPLES]
y_train_hinge_sub = y_train_hinge[:NUM_EXAMPLES]
```

Training this model to convergence should achieve >85% accuracy on the test set.

```python
qnn_history = model.fit(
      x_train_tfcirc_sub, y_train_hinge_sub,
      batch_size=32,
      epochs=EPOCHS,
      verbose=1,
      validation_data=(x_test_tfcirc, y_test_hinge))

qnn_results = model.evaluate(x_test_tfcirc, y_test)
```

Note: The training accuracy reports the average over the epoch. The validation accuracy is evaluated at the end of each epoch.

## 3. Classical neural network

While the quantum neural network works for this simplified MNIST problem, a basic classical neural network can easily outperform a QNN on this task. After a single epoch, a classical neural network can achieve >98% accuracy on the holdout set.

In the following example, a classical neural network is used for for the 3-6 classification problem using the entire 28x28 image instead of subsampling the image. This easily converges to nearly 100% accuracy of the test set.

```python
def create_classical_model():
    # A simple model based off LeNet from https://keras.io/examples/mnist_cnn/
    model = tf.keras.Sequential()
    model.add(tf.keras.layers.Conv2D(32, [3, 3], activation='relu', input_shape=(28,28,1)))
    model.add(tf.keras.layers.Conv2D(64, [3, 3], activation='relu'))
    model.add(tf.keras.layers.MaxPooling2D(pool_size=(2, 2)))
    model.add(tf.keras.layers.Dropout(0.25))
    model.add(tf.keras.layers.Flatten())
    model.add(tf.keras.layers.Dense(128, activation='relu'))
    model.add(tf.keras.layers.Dropout(0.5))
    model.add(tf.keras.layers.Dense(1))
    return model

model = create_classical_model()
model.compile(loss=tf.keras.losses.BinaryCrossentropy(from_logits=True),
              optimizer=tf.keras.optimizers.Adam(),
              metrics=['accuracy'])

model.summary()
```

```python
model.fit(x_train,
          y_train,
          batch_size=128,
          epochs=1,
          verbose=1,
          validation_data=(x_test, y_test))

cnn_results = model.evaluate(x_test, y_test)
```

The above model has nearly 1.2M parameters. For a more fair comparison, try a 37-parameter model, on the subsampled images:

```python
def create_fair_classical_model():
    # A simple model based off LeNet from https://keras.io/examples/mnist_cnn/
    model = tf.keras.Sequential()
    model.add(tf.keras.layers.Flatten(input_shape=(4,4,1)))
    model.add(tf.keras.layers.Dense(2, activation='relu'))
    model.add(tf.keras.layers.Dense(1))
    return model

model = create_fair_classical_model()
model.compile(loss=tf.keras.losses.BinaryCrossentropy(from_logits=True),
              optimizer=tf.keras.optimizers.Adam(),
              metrics=['accuracy'])

model.summary()
```

```python
model.fit(x_train_bin,
          y_train_nocon,
          batch_size=128,
          epochs=20,
          verbose=2,
          validation_data=(x_test_bin, y_test))

fair_nn_results = model.evaluate(x_test_bin, y_test)
```

## 4. Comparison

Higher resolution input and a more powerful model make this problem easy for the CNN. While a classical model of similar power (~32 parameters) trains to a similar accuracy in a fraction of the time. One way or the other, the classical neural network easily outperforms the quantum neural network. For classical data, it is difficult to beat a classical neural network.

```python
qnn_accuracy = qnn_results[1]
cnn_accuracy = cnn_results[1]
fair_nn_accuracy = fair_nn_results[1]

sns.barplot(["Quantum", "Classical, full", "Classical, fair"],
            [qnn_accuracy, cnn_accuracy, fair_nn_accuracy])
```