 Tutorial 2

 Physics Informed Neural Networks Part 4

 Navier Stokes Hidden Fluid Mechanics Example

# Overview

This notebook is based on two papers: *[Physics-Informed Neural Networks:  A Deep LearningFramework for Solving Forward and Inverse ProblemsInvolving Nonlinear Partial Differential Equations](https://www.sciencedirect.com/science/article/pii/S0021999118307125)* and *[Hidden Physics Models:  Machine Learning of NonlinearPartial Differential Equations](https://www.sciencedirect.com/science/article/pii/S0021999117309014)* with the help of  Fergus Shone and Michael Macraild.

These tutorials will go through solving Partial Differential Equations using Physics Informed Neuaral Networks focusing on the 1D Heat Equation and a more complex example using the Navier Stokes Equation

**This notebook is a breif illustrative overview of Hidden Physics Models beyond the scope of these tutorials**

If you have not already then in your repositoy directory please run the following code in your terminal (linux or Mac) or via git bash.

```bash
git submodule init
git submodule update --init --recursive
```

 **If this does not work please clone the [PINNs](https://github.com/maziarraissi/PINNs) repository into your Physics_Informed_Neural_Networks folder**

Physics Informed Neural Networks

For a typical Neural Network using algorithims like gradient descent to look for a hypothesis, data is the only guide, however if the data is noisy or sparse and we already have governing physical models we can use the knowledge we already know to optamize and inform the algoithms. This can be done via [feature enginnering]() or by adding a physicall inconsistency term to the loss function.

## The very basics

If you know nothing about neural networks there is a [toy neural network python code example](https://github.com/cemac/LIFD_ENV_ML_NOTEBOOKS/tree/main/ToyNeuralNetwork) included in the [LIFD ENV ML Notebooks Repository]( https://github.com/cemac/LIFD_ENV_ML_NOTEBOOKS). Creating a 2 layer neural network to illustrate the fundamentals of how Neural Networks work and the equivlent code using the python machine learning library [tensorflow](https://keras.io/).

## Recommended reading

The in-depth theory behind neural networks will not be covered here as this tutorial is focusing on application of machine learning methods. If you wish to learn more here are some great starting points.

* [All you need to know on Neural networks](https://towardsdatascience.com/nns-aynk-c34efe37f15a)
* [Introduction to Neural Networks](https://victorzhou.com/blog/intro-to-neural-networks/)
* [Physics Guided Neural Networks](https://towardsdatascience.com/physics-guided-neural-networks-pgnns-8fe9dbad9414)
* [Maziar Rassi's Physics informed GitHub web Page](https://maziarraissi.github.io/PINNs/)

# Hidden Fluid Mechanics #

In this notebook, we will utilise a more advanced implementation of PINNs, taken from Maziar Raissi's paper Hidden Fluid Mechanics. In many fluid flow scenarios, direct measurement of variables such as velocity and pressure is not possible; However, we may have access to measurements of some passive scalar field (such as smoke concentration in wind tunnel testing). This work aims to use PINNs to uncover hidden velocity and pressure fields for flow problems, utilising data drawn only from measurements of some passive scalar, $c(t,x,y)$. This scalar is governed by the transport equation:

\begin{equation}
c_t + u c_x + v c_y = \text{Pec}^{-1} \left(c_{xx} + c_{yy}\right)
\end{equation}

where $\mathrm{Pec}$ is the Peclet number, and $u, v$ are the two velocity components. Then, the Navier-Stokes equations are given by:

\begin{equation}
u_t + uu_x + vu_y = -p_x +\mathrm{Re}^{-1}(u_{xx} + u_{yy}),
\\
v_t + uv_x + vv_y = -p_y +\mathrm{Re}^{-1}(v_{xx} + v_{yy}),
\\
u_x + v_y = 0
\end{equation}

for pressure $p$ and Reynolds number $\mathrm{Re}$. Note that the only constraint on the velocity and pressure predictions is that they satisfy the above equations.

The structure of the neural network is seen below:

The network has four inputs, as expected, and six outputs, namely the three velocity components, the pressure, the concentration, c, and one final variable, d. d(t,x,y,z) is an 'auxilliary variable', defined to be the complement of c (i.e. d = 1 - c), and is governed by the same transport equation as c. Its inclusion improves prediction accuracy, and helps in detecting boundary locations.

The data for this notebook was generated by Raissi et al. using a spectral element sovler called NekTar. The Navier-Stokes and transport equations are numerically approximated to a high degree of accuracy.

The fluid problem at hand is 2D channel flow over an obstacle. A crude diagram of the flow domain can be seen below:

It may be useful to read the original paper alongside this notebook, which can be found at https://arxiv.org/pdf/1808.04327.pdf. The source code can be accessed via this Github repository: https://github.com/maziarraissi/HFM.

This script was orignally written by Maziar Raissi et al. in their PINNs repository, before being modified by Michael MacRaild and Fergus Shone. All figures are from https://arxiv.org/pdf/1808.04327.pdf.

 Python

## Tensorflow

There are many machine learning python libraries available, [TensorFlow](https://www.tensorflow.org/) a is one such library. If you have GPUs on the machine you are using TensorFlow will automatically use them and run the code even faster!

## Further Reading

* [Running Jupyter Notebooks](https://jupyter.readthedocs.io/en/latest/running.html#running)
* [Tensorflow optimizers](https://www.tutorialspoint.com/tensorflow/tensorflow_optimizers.htm)

 Requirements

These notebooks should run with the following requirements satisfied

 Python Packages:

* Python 3
* tensorflow > 2
* numpy
* matplotlib
* scipy

 Data Requirements

This notebook referes to some data included in the git hub repositroy

**Contents:**

1. [1D Heat Equation Non ML Example](PINNs_1DHeatEquations_nonML.ipynb)
2. [1D Equation PINN Example](PINNs_1DHeatEquationExample.ipynb)
3. [Navier-Stokes PINNs discovery of PDE’s](PINNs_NavierStokes_example.ipynb)
3. **[Navier-Stokes PINNs Hidden Fluid Mechanics](PINNs_Navier_Stokes_HFM.ipynb)**

Load in all required modules (includig some auxillary code) and turn off warnings. Make sure Keras session is clear

```python
# For readability: disable warnings
import warnings
warnings.filterwarnings('ignore')
```

```python
import sys
sys.path.insert(0, 'PINNs/Utilities/')
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
import scipy.io
from scipy.interpolate import griddata
from itertools import product, combinations
from mpl_toolkits.mplot3d import Axes3D
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
from mpl_toolkits.axes_grid1 import make_axes_locatable
import matplotlib.gridspec as gridspec
from time import time
import scipy.sparse as sp
import scipy.sparse.linalg as la
```

# Define PINN Functions

In this section we define a number of functions to be called later in the script, such as error metrics, the general neural network class etc. In the original github repository, these functions are defined in a separate script called 'utilities.py', which is called in each example script. To keep this notebook contained, we include them here instead.

The basic functionality of this code is the same as the previous examples, but some of the syntax is updated to streamline the code.

```python
# generate tensorflow session and initialise variables
def tf_session():
    # tf session
    config = tf.compat.v1.ConfigProto(allow_soft_placement=True,
                            log_device_placement=True)
    config.gpu_options.force_gpu_compatible = True
    sess = tf.compat.v1.Session(config=config)

    # init
    init = tf.compat.v1.global_variables_initializer()
    sess.run(init)

    return sess

# produce relative error between prediction and ground truth
def relative_error(pred, exact):
    if type(pred) is np.ndarray:
        return np.sqrt(np.mean(np.square(pred - exact))/np.mean(np.square(exact - np.mean(exact))))
    return tf.sqrt(tf.reduce_mean(tf.square(pred - exact))/tf.reduce_mean(tf.square(exact - tf.reduce_mean(exact))))

# produce MSE of predicition and ground truth
def mean_squared_error(pred, exact):
    if type(pred) is np.ndarray:
        return np.mean(np.square(pred - exact))
    return tf.reduce_mean(tf.square(pred - exact))

# generate gradient of Y w.r.t. x using automatic differentiation
def fwd_gradients(Y, x):
    dummy = tf.ones_like(Y)
    G = tf.compat.v1.gradients(Y, x, grad_ys=dummy, colocate_gradients_with_ops=True)[0]
    Y_x = tf.compat.v1.gradients(G, dummy, colocate_gradients_with_ops=True)[0]
    return Y_x

# calculate 2D velocity gradients
def Gradient_Velocity_2D(u, v, x, y):

    Y = tf.concat([u, v], 1)

    Y_x = fwd_gradients(Y, x)
    Y_y = fwd_gradients(Y, y)

    u_x = Y_x[:,0:1]
    v_x = Y_x[:,1:2]

    u_y = Y_y[:,0:1]
    v_y = Y_y[:,1:2]

    return [u_x, v_x, u_y, v_y]

# basic neural network class. This function constructs the network architecture,
# normalises the inputs using the mean and standard deviation and specifies the
# activation function, which is chosen here to be sigmoid.
class neural_net(object):
    def __init__(self, *inputs, layers):

        self.layers = layers
        self.num_layers = len(self.layers)

        if len(inputs) == 0:
            in_dim = self.layers[0]
            self.X_mean = np.zeros([1, in_dim])
            self.X_std = np.ones([1, in_dim])
        else:
            X = np.concatenate(inputs, 1)
            self.X_mean = X.mean(0, keepdims=True)
            self.X_std = X.std(0, keepdims=True)

        self.weights = []
        self.biases = []
        self.gammas = []

        for l in range(0,self.num_layers-1):
            in_dim = self.layers[l]
            out_dim = self.layers[l+1]
            W = np.random.normal(size=[in_dim, out_dim])
            b = np.zeros([1, out_dim])
            g = np.ones([1, out_dim])
            # tensorflow variables
            self.weights.append(tf.Variable(W, dtype=tf.float32, trainable=True))
            self.biases.append(tf.Variable(b, dtype=tf.float32, trainable=True))
            self.gammas.append(tf.Variable(g, dtype=tf.float32, trainable=True))

    def __call__(self, *inputs):

        H = (tf.concat(inputs, 1) - self.X_mean)/self.X_std

        for l in range(0, self.num_layers-1):
            W = self.weights[l]
            b = self.biases[l]
            g = self.gammas[l]
            # weight normalization
            V = W/tf.norm(W, axis = 0, keepdims=True)
            # matrix multiplication
            H = tf.matmul(H, V)
            # add bias
            H = g*H + b
            # activation
            if l < self.num_layers-2:
                H = H*tf.sigmoid(H)

        Y = tf.split(H, num_or_size_splits=H.shape[1], axis=1)

        return Y

# produce the necessary output gradients and then form the Navier-Stokes residuals for 2D flow
def Navier_Stokes_2D(c, u, v, p, t, x, y, Pec, Rey):

    Y = tf.concat([c, u, v, p], 1)

    Y_t = fwd_gradients(Y, t)
    Y_x = fwd_gradients(Y, x)
    Y_y = fwd_gradients(Y, y)
    Y_xx = fwd_gradients(Y_x, x)
    Y_yy = fwd_gradients(Y_y, y)

    c = Y[:,0:1]
    u = Y[:,1:2]
    v = Y[:,2:3]
    p = Y[:,3:4]

    c_t = Y_t[:,0:1]
    u_t = Y_t[:,1:2]
    v_t = Y_t[:,2:3]

    c_x = Y_x[:,0:1]
    u_x = Y_x[:,1:2]
    v_x = Y_x[:,2:3]
    p_x = Y_x[:,3:4]

    c_y = Y_y[:,0:1]
    u_y = Y_y[:,1:2]
    v_y = Y_y[:,2:3]
    p_y = Y_y[:,3:4]

    c_xx = Y_xx[:,0:1]
    u_xx = Y_xx[:,1:2]
    v_xx = Y_xx[:,2:3]

    c_yy = Y_yy[:,0:1]
    u_yy = Y_yy[:,1:2]
    v_yy = Y_yy[:,2:3]

    e1 = c_t + (u*c_x + v*c_y) - (1.0/Pec)*(c_xx + c_yy)
    e2 = u_t + (u*u_x + v*u_y) + p_x - (1.0/Rey)*(u_xx + u_yy)
    e3 = v_t + (u*v_x + v*v_y) + p_y - (1.0/Rey)*(v_xx + v_yy)
    e4 = u_x + v_y

    return e1, e2, e3, e4

# calculate the strain rate for generating results later in the script
def Strain_Rate_2D(u, v, x, y):

    [u_x, v_x, u_y, v_y] = Gradient_Velocity_2D(u, v, x, y)

    eps11dot = u_x
    eps12dot = 0.5*(v_x + u_y)
    eps22dot = v_y

    return [eps11dot, eps12dot, eps22dot]
```

# Define PINN (HFM) Class

Here the main PINN class is defined. It is shorter than the PINN class in the previous notebooks as many of the functions that were previously included inside are defined externally in the previous section, and called within this class. There is also a change in the way the network is trained, with the implementation of batch training (or mini-batch training). This is useful when very large amounts of data are used, as it divides the entire training set into subsets called batches, avoiding issues with exceeding data allocation. Also, when implemented correctly, batch training can be more efficient. It is worth reading up on batch training in general, as it is a useful machine learning approach.

```python
class HFM(object):
    # notational conventions
    # _tf: placeholders for input/output data and points used to regress the equations
    # _pred: output of neural network
    # _eqns: points used to regress the equations
    # _data: input-output data
    # _star: preditions

    def __init__(self, t_data, x_data, y_data, c_data,
                       t_eqns, x_eqns, y_eqns,
                       layers, batch_size):

        # specs
        self.layers = layers
        self.batch_size = batch_size

        # flow properties
        self.Pec = tf.Variable(15.0, dtype=tf.float32, trainable = True)
        self.Rey = tf.Variable(5.0, dtype=tf.float32, trainable = True)

        # data
        [self.t_data, self.x_data, self.y_data, self.c_data] = [t_data, x_data, y_data, c_data]
        [self.t_eqns, self.x_eqns, self.y_eqns] = [t_eqns, x_eqns, y_eqns]

        # placeholders
        [self.t_data_tf, self.x_data_tf, self.y_data_tf, self.c_data_tf] = [tf.compat.v1.placeholder(tf.float32, shape=[None, 1]) for _ in range(4)]
        [self.t_eqns_tf, self.x_eqns_tf, self.y_eqns_tf] = [tf.compat.v1.placeholder(tf.float32, shape=[None, 1]) for _ in range(3)]

        # physics "uninformed" neural networks
        self.net_cuvp = neural_net(self.t_data, self.x_data, self.y_data, layers = self.layers)

        [self.c_data_pred,
         self.u_data_pred,
         self.v_data_pred,
         self.p_data_pred] = self.net_cuvp(self.t_data_tf,
                                           self.x_data_tf,
                                           self.y_data_tf)

        # physics "informed" neural networks
        [self.c_eqns_pred,
         self.u_eqns_pred,
         self.v_eqns_pred,
         self.p_eqns_pred] = self.net_cuvp(self.t_eqns_tf,
                                           self.x_eqns_tf,
                                           self.y_eqns_tf)

        [self.e1_eqns_pred,
         self.e2_eqns_pred,
         self.e3_eqns_pred,
         self.e4_eqns_pred] = Navier_Stokes_2D(self.c_eqns_pred,
                                               self.u_eqns_pred,
                                               self.v_eqns_pred,
                                               self.p_eqns_pred,
                                               self.t_eqns_tf,
                                               self.x_eqns_tf,
                                               self.y_eqns_tf,
                                               self.Pec,
                                               self.Rey)

        [self.eps11dot_eqns_pred,
         self.eps12dot_eqns_pred,
         self.eps22dot_eqns_pred] = Strain_Rate_2D(self.u_eqns_pred,
                                                   self.v_eqns_pred,
                                                   self.x_eqns_tf,
                                                   self.y_eqns_tf)

        # loss
        self.loss = mean_squared_error(self.c_data_pred, self.c_data_tf) + \
                    mean_squared_error(self.e1_eqns_pred, 0.0) + \
                    mean_squared_error(self.e2_eqns_pred, 0.0) + \
                    mean_squared_error(self.e3_eqns_pred, 0.0) + \
                    mean_squared_error(self.e4_eqns_pred, 0.0)

        # optimizers
        self.learning_rate = tf.compat.v1.placeholder(tf.float32, shape=[])
        self.optimizer = tf.compat.v1.train.AdamOptimizer(learning_rate = self.learning_rate)
        self.train_op = self.optimizer.minimize(self.loss)

        self.sess = tf_session()

        # Add ops to save and restore all the variables.
        self.netSaveDir = "modelckpts/HFM/"
        self.saver = tf.compat.v1.train.Saver()

    def train(self, total_time, learning_rate):

        N_data = self.t_data.shape[0]
        N_eqns = self.t_eqns.shape[0]

        start_time = time()
        running_time = 0
        it = 0
        while running_time < total_time:

            idx_data = np.random.choice(N_data, min(self.batch_size, N_data))
            idx_eqns = np.random.choice(N_eqns, self.batch_size)

            (t_data_batch,
             x_data_batch,
             y_data_batch,
             c_data_batch) = (self.t_data[idx_data,:],
                              self.x_data[idx_data,:],
                              self.y_data[idx_data,:],
                              self.c_data[idx_data,:])

            (t_eqns_batch,
             x_eqns_batch,
             y_eqns_batch) = (self.t_eqns[idx_eqns,:],
                              self.x_eqns[idx_eqns,:],
                              self.y_eqns[idx_eqns,:])

            tf_dict = {self.t_data_tf: t_data_batch,
                       self.x_data_tf: x_data_batch,
                       self.y_data_tf: y_data_batch,
                       self.c_data_tf: c_data_batch,
                       self.t_eqns_tf: t_eqns_batch,
                       self.x_eqns_tf: x_eqns_batch,
                       self.y_eqns_tf: y_eqns_batch,
                       self.learning_rate: learning_rate}

            self.sess.run([self.train_op], tf_dict)

            # Print
            if it % 10 == 0:
                elapsed = time() - start_time
                running_time += elapsed/3600.0
                [loss_value,
                 Pec_value,
                 Rey_value,
                 learning_rate_value] = self.sess.run([self.loss,
                                                       self.Pec,
                                                       self.Rey,
                                                       self.learning_rate], tf_dict)
                print('It: %d, Loss: %.3e, Pec: %.3f, Rey: %.3f, Time: %.2fs, Running Time: %.2fh, Learning Rate: %.1e'
                      %(it, loss_value, Pec_value, Rey_value, elapsed, running_time, learning_rate_value))
                sys.stdout.flush()
                start_time = time()
            it += 1

            if it % 1000 == 0:
                save_path = self.saver.save(self.sess, self.netSaveDir + 'model_at_iter%s.ckpt'%(it))
                print('Model saved in path: %s' % save_path)
        save_path = self.saver.save(self.sess, self.netSaveDir + 'model.ckpt')
        print('Model saved in path: %s' % save_path)

    def predict(self, t_star, x_star, y_star):
        tf_dict = {self.t_data_tf: t_star, self.x_data_tf: x_star, self.y_data_tf: y_star}

        c_star = self.sess.run(self.c_data_pred, tf_dict)
        u_star = self.sess.run(self.u_data_pred, tf_dict)
        v_star = self.sess.run(self.v_data_pred, tf_dict)
        p_star = self.sess.run(self.p_data_pred, tf_dict)

        return c_star, u_star, v_star, p_star

    def predict_eps_dot(self, t_star, x_star, y_star):

        tf_dict = {self.t_eqns_tf: t_star, self.x_eqns_tf: x_star, self.y_eqns_tf: y_star}

        eps11dot_star = self.sess.run(self.eps11dot_eqns_pred, tf_dict)
        eps12dot_star = self.sess.run(self.eps12dot_eqns_pred, tf_dict)
        eps22dot_star = self.sess.run(self.eps22dot_eqns_pred, tf_dict)

        return eps11dot_star, eps12dot_star, eps22dot_star
```

# Main Code

In this section we run each of our previously defined functions and classes to train the network and generate results. We specify the batch size, network width and depth, process the training and test data, train the network and output error results. Note that the 'train' function is commented out, so if you would like to re-train the network feel free to uncomment that line (although be warned: it will overwrite the previously trained weights).

```python
# This line of code is required to prevent some tensorflow errors arrising from the
# inclusion of some tensorflw v 1 code
tf.compat.v1.disable_eager_execution()

batch_size = 10000

layers = [3] + 10*[4*50] + [4]

# Load Data
data = scipy.io.loadmat('Data/Stenosis2D.mat')

t_star = data['t_star'] # T x 1
x_star = data['x_star'] # N x 1
y_star = data['y_star'] # N x 1

T = t_star.shape[0]
N = x_star.shape[0]

U_star = data['U_star'] # N x T
V_star = data['V_star'] # N x T
P_star = data['P_star'] # N x T
C_star = data['C_star'] # N x T

# Rearrange Data
T_star = np.tile(t_star, (1,N)).T # N x T
X_star = np.tile(x_star, (1,T)) # N x T
Y_star = np.tile(y_star, (1,T)) # N x T

######################################################################
######################## Noiseless Data ##############################
######################################################################

T_data = T # int(sys.argv[1])
N_data = N # int(sys.argv[2])
idx_t = np.concatenate([np.array([0]), np.random.choice(T-2, T_data-2, replace=False)+1, np.array([T-1])] )
idx_x = np.random.choice(N, N_data, replace=False)
t_data = T_star[:, idx_t][idx_x,:].flatten()[:,None]
x_data = X_star[:, idx_t][idx_x,:].flatten()[:,None]
y_data = Y_star[:, idx_t][idx_x,:].flatten()[:,None]
c_data = C_star[:, idx_t][idx_x,:].flatten()[:,None]

T_eqns = T
N_eqns = N
idx_t = np.concatenate([np.array([0]), np.random.choice(T-2, T_eqns-2, replace=False)+1, np.array([T-1])] )
idx_x = np.random.choice(N, N_eqns, replace=False)
t_eqns = T_star[:, idx_t][idx_x,:].flatten()[:,None]
x_eqns = X_star[:, idx_t][idx_x,:].flatten()[:,None]
y_eqns = Y_star[:, idx_t][idx_x,:].flatten()[:,None]
```

```python
# Training
model = HFM(t_data, x_data, y_data, c_data,
            t_eqns, x_eqns, y_eqns,
            layers, batch_size)
```

# Loading Pre trained model option

If the training time is too slow you can skip the following line and load in a pretrained model instead set `loadweights = True` in the next cell. You can play around with different number of iterations to see the effects e.g. setting `saver.restore(sess, netSaveDir + 'model_at_iter3000.ckpt')`

```python
# model.train(total_time = 40, learning_rate=1e-3) # UNCOMMENT THIS LINE IF YOU WOULD LIKE TO RE-TRAIN
```

```python
loadweights = True
if loadweights:
    print("loading pre trained model")
    netSaveDir = "modelckpts/HFM/"
    saver = tf.compat.v1.train.Saver()
    saver.restore(model.sess, netSaveDir + 'model_at_iter3000.ckpt')
```

```python
Shear = np.zeros((300,t_star.shape[0]))

for snap in range(0,t_star.shape[0]):

    x1_shear = np.linspace(15,25,100)[:,None]
    x2_shear = np.linspace(25,35,100)[:,None]
    x3_shear = np.linspace(35,55,100)[:,None]

    x_shear = np.concatenate([x1_shear,x2_shear,x3_shear], axis=0)

    y1_shear = 0.0*x1_shear
    y2_shear = np.sqrt(25.0 - (x2_shear - 30.0)**2)
    y3_shear = 0.0*x3_shear

    y_shear = np.concatenate([y1_shear,y2_shear,y3_shear], axis=0)

    t_shear = T_star[0,snap] + 0.0*x_shear

    eps11_dot_shear, eps12_dot_shear, eps22_dot_shear = model.predict_eps_dot(t_shear, x_shear, y_shear)

    nx1_shear = 0.0*x1_shear
    nx2_shear = 6.0 - x2_shear/5.0
    nx3_shear = 0.0*x3_shear

    nx_shear = np.concatenate([nx1_shear,nx2_shear,nx3_shear], axis=0)

    ny1_shear = -1.0 + 0.0*y1_shear
    ny2_shear = -y2_shear/5.0
    ny3_shear = -1.0 + 0.0*y3_shear

    ny_shear = np.concatenate([ny1_shear,ny2_shear,ny3_shear], axis=0)

    shear_x = 2.0*(1.0/5.0)*(eps11_dot_shear*nx_shear + eps12_dot_shear*ny_shear)
    shear_y = 2.0*(1.0/5.0)*(eps12_dot_shear*nx_shear + eps22_dot_shear*ny_shear)

    shear = np.sqrt(shear_x**2 + shear_y**2)

    Shear[:,snap] = shear.flatten()
```

```python
# Test Data
snap = np.array([55])
t_test = T_star[:,snap]
x_test = X_star[:,snap]
y_test = Y_star[:,snap]

c_test = C_star[:,snap]
u_test = U_star[:,snap]
v_test = V_star[:,snap]
p_test = P_star[:,snap]
```

```python
# Prediction
c_pred, u_pred, v_pred, p_pred = model.predict(t_test, x_test, y_test)
```

```python
# Error
error_c = relative_error(c_pred, c_test)
error_u = relative_error(u_pred, u_test)
error_v = relative_error(v_pred, v_test)
error_p = relative_error(p_pred - np.mean(p_pred), p_test - np.mean(p_test))

print('Error c: %e' % (error_c))
print('Error u: %e' % (error_u))
print('Error v: %e' % (error_v))
print('Error p: %e' % (error_p))
```

```python
################# Save Data ###########################

C_pred = 0*C_star
U_pred = 0*U_star
V_pred = 0*V_star
P_pred = 0*P_star
```

```python
# calculate the relative error of each variable per time-step
for snap in range(0,t_star.shape[0]):
    t_test = T_star[:,snap:snap+1]
    x_test = X_star[:,snap:snap+1]
    y_test = Y_star[:,snap:snap+1]

    c_test = C_star[:,snap:snap+1]
    u_test = U_star[:,snap:snap+1]
    v_test = V_star[:,snap:snap+1]
    p_test = P_star[:,snap:snap+1]

    # Prediction
    c_pred, u_pred, v_pred, p_pred = model.predict(t_test, x_test, y_test)

    C_pred[:,snap:snap+1] = c_pred
    U_pred[:,snap:snap+1] = u_pred
    V_pred[:,snap:snap+1] = v_pred
    P_pred[:,snap:snap+1] = p_pred

    # Error
    error_c = relative_error(c_pred, c_test)
    error_u = relative_error(u_pred, u_test)
    error_v = relative_error(v_pred, v_test)
    error_p = relative_error(p_pred - np.mean(p_pred), p_test - np.mean(p_test))
    print('Time step: ', snap)
    print('Error c: %e' % (error_c))
    print('Error u: %e' % (error_u))
    print('Error v: %e' % (error_v))
    print('Error p: %e' % (error_p))

```

# Plots

Since the original code included no Python-based plotting functionality, the plots have been taken from the original [paper](https://www.sciencedirect.com/science/article/pii/S0021999117309014). Here we see, at one time step, the concentration fields c and d. The inverted nature of these two variables can be seen.

In the plots below, we see comparisons between ground truth and prediction for each output variable at one time step. We see that the PINN is capable of replicating each variable with a good degree of accuracy, although it is worth noting that the pressure prediction can only be accurate up to a constant, as it only appears in the Navier-Stokes equation in derivative form. This discrepency can be seen in the colourbar alongside the pressure plots.

Finally, plots for ground truth and predicted wall shear stress (WSS) can be found below. Here, plotted is the WSS magnitude on the lower boundary of the domain at every time step. What is most impressive here is that we have not imposed any boundary conditions on the lower wall and we still obtain very accurate predictions based only on information from within the flow.

```python

```


#   Linear Algebraic Systems

#  Learning Objectives

### *Setting up a linear system*
- Be able to distinguish a linear system from a nonlinear system.
- Learn to write a linear system in a matrix-vector format.
- Input a linear system into a program
- Write applicable engineering models in a linear format.

### *Characterizing linear systems*
- Determine the bandwidth(s) of the linear system.*
- Determine if a linear system has a unique solution.
- Will solving a linear system provide a meaningful result (is it well-posed).

### *Solving a linear system*
- Solve a linear system by Gauss-Elimination
- Solve a linear system by LU factorizations.
- State when numerical errors occur may occur in Gauss-Elimination and how to resolve them.\*
- Be able to state some conditions for the convergence of Jacobi and Gauss-Siedel methods.\*

\*Not covered in tutorial.

#   Relevance

It would be only a slight overstatement to say that linear algebra underlies all modern numerical algorithms to one degree or another. Even software which specifically addresses nonlinear or complicated forms will often make use of linear algebra in a myriad of subroutines. In essence, many complicated problems can be reduced to repeated formulating and solving linear systems.

#  Chlorination of Benzene

Monochlorobenzene is a high boiling point solvent used in many industrial applications ranging from the manufacture of [dyes](https://patentimages.storage.googleapis.com/76/55/12/31756682f755c5/US6013811.pdf), [plastic and rubber processing](https://patentimages.storage.googleapis.com/66/c8/e1/69918f557fbd35/US8563079.pdf), [paints](https://patentimages.storage.googleapis.com/28/17/a7/a40e46f75806cf/US9062206.pdf), and [waxes](https://patents.google.com/patent/US3999960A/en). Approximately, [$1 billion](https://www.prnewswire.com/news-releases/global-chlorobenzene-industry-300939916.html) worth of this solvent are used per year. It is [produced by combining chlorine with benzene](https://onlinelibrary.wiley.com/doi/abs/10.1002/14356007.o06_o03) in the presence of a Lewis acid catalyst such as Ferric Chloride FeCl3 or Anhydrous Aluminum Chloride AlCl3. When benzene and chlorine are mixed two key reactions occur: (1) the chlorination of benzene and (2) the secondary undesirable chlorination of the resulting monochlorobenzene. Product purity is ensured by performing a series of separation steps after the initial reaction and reagant usage is reduced by including a recycle loop.

Objective: A large customer order has been placed for monochlorobenzene that needs to be fulfilled on a tight timeline. The small chemical company you work for has occasionally produced this solvent in the past and has an existing 6m3 continuously stirred tank reactor (CSTR) that will need to be repurposed to produce 25kmol/h monochlorobenzene. Moreover, there isn't sufficient development time available to attempt to improve the processing conditions for the reactor or separators. As a result, you'll need to use the documented process conditions for the reactor and separators which means supplying to the first separator an effluent consisting of 90% benzene, 7% monochlorobenzene, and 3% dichlorobenzene by mole. We need to determine the recycle flow rates and reaction rates required needed to achieve the desired rate at which monochlorobenzene need to be produced.

Denote benzene as (A), monochlorobenzene as (B), and dichlorobenzene as (C). The mole fraction of species A in stream S1 is denote by y1,A and the molar flowrate F [kmol/h] for stream S1 is denoted F1.

- The feed stream is composed entirely of A and Cl2.
- An excess amount of Cl2 is provided and HCl removal can be assumed to be trivial. So it's reasonable exclude it from our design considerations. Moreover, this means the reactions which take place in the reactor vessel can be simplified to A → B (at rate r1 [kmol/h]) and B → C (at rate r2 [kmol/h]).

#  Variables and Mass Balances

For streams containing only a pure substance, we need only keep track of the total flow rate. Enumerating each quantity in the model is given by $F_1$, $F_2$, $F_3$, $F_4$, $F_5$, $F_6$, $F_7$, $y_{3,A}$, $y_{3,B}$, $y_{3,C}$, $y_{4,B}$, $y_{4,C}$, $r_1$, $r_2$, and $\nu$. Five of these are specified (e.g. these are **parameters** with fixed values). This leaves ten degrees of freedom associated with a variable. We'll now need to write out the equations which govern the system (physical laws, equipment specifications, and performance specifications). We'll start with writing the mass balances over each unit operation.

Mixer (Overall Mass Balance):

\begin{align}
    F_1 + F_7 - F_2 &= 0
\end{align}

Reactor (Individual Component Mass Balances):

\begin{align}
    F_2 - r_1\nu - y_{3,A}F_3 &= 0 \\
    (r_1 - r_2)\nu + y_{3,B}F_3 &= 0 \\
    r_2\nu  - y_{3,C}F_3 &= 0
\end{align}

Separator #1 (Individual Component Mass Balances):

\begin{align}
    F_3 - F_4 - F_7 &= 0 \\
    y_{3,B}F_3 - y_{4,B}F_4 &= 0 \\
    y_{3,C}F_3 - y_{4,C}F_4 &= 0
\end{align}

Separator #2 (Individual Component Mass Balances):

\begin{align}
    F_4 - F_5 - F_6 &= 0 \\
    y_{4,B}F_4 - F_5 &= 0
\end{align}

In order to write these equations in a matrix-vector form: we'll first need to associate each variable (excluding **parameters**) in the model with a component in a vector of variables, $\mathbf{x}$.

\begin{align}
    \mathbf{x} = (F_1, F_2, F_3, F_4, F_6, F_7, y_{4,B}, y_{4,C}, r_1, r_2)
\end{align}

So, $x_2 = F_2$, $x_3 = F_3$ and so on until we have $x_{10} = r_2$.

Now, take a minute to inspect the model and categorize the system of equations.

The 2nd and 3rd equations for Separator #1 as well as the 2nd equation in Separator #2 contain the expressions $y_{4,B}F_4$ and $y_{4,C}F_4$. These terms consist of two variables being multiplied by one another (a bilinear term) which is one of simplest commonly encountered nonlinear term. All other expressions consist of addition, subtraction, and the multiplication of a parameter by a variable. So this system of equations is nonlinear but would be linear if we ommitted the expressions: $y_{4,B}F_4$ and $y_{4,C}F_4$.

It's often useful to reformulate a model (algebraicly rearrange and potentially introduce new variables) to arrive at a more easily solvable form.

We'll start by introducing two new variables: the molar flowrates $F_{4,B}$ and $F_{4,C}$ defined as by

\begin{align}
    F_{4,B} = y_{4,B}F_4 \\
    F_{4,C} = y_{4,C}F_4
\end{align}

We can then write an additional equation for the molar flowrates of S4:

Molar flowrate expressions for S4:

\begin{align}
    F_{4,B} + F_{4,C} &= F_4
\end{align}

Next, we'll introduce the variables: $F_{3,A}$, $F_{3,B}$, and $F_{3,C}$ defined as:

\begin{align}
    F_{3,A} = y_{3,A}F_3 \\
    F_{3,B} = y_{3,B}F_3 \\
    F_{3,C} = y_{3,C}F_3
\end{align}

We can then write an additional equation for the molar flowrates of S3:

Molar flowrate expressions for S3:

\begin{align}
F_{3,A} + F_{3,B} + F_{3,C} = F_3
\end{align}

We can then rearrange the mass balances for the Reactor, Separator #1, and Separator #2.

Reactor (Individual Component Mass Balances):

\begin{align}
    F_2 - r_1\nu - F_{3,A} &= 0 \\
    (r_1 - r_2)\nu + F_{3,B} &= 0 \\
    r_2\nu  - F_{3,C} &= 0
\end{align}

Reformulated Separator #1 Mass Balances:

\begin{align}
    F_3 - F_4 - F_7 &= 0 \\
    F_{3,B} - F_{4,B} &= 0 \\
    F_{3,C} - F_{4,C} &= 0
\end{align}

Reformulated Separator #2 Mass Balances:

\begin{align}
    F_4 - F_5 - F_6 &= 0 \\
    F_{4,B} - F_5 &= 0
\end{align}

\begin{align}
    \mathbf{x'} = (F_1, F_2, F_{3,A}, F_{3,B}, F_{3,C}, F_3, F_{4,B}, F_{4,C}, F_4, F_6, F_7, r_1, r_2)
\end{align}

INTERACTIVE! Now we enter the above linear equation, Mx' = b  in matrix-vector form.

```julia
# Define the Mx' = b linear system

# Define storage for linear system
M = zeros(13,13)    # Makes an 2d array of size 13-by-13. This array has 13 rows and 13 columns.
b = zeros(13)       # Makes a column vector of size 10. A vector with 13 rows and 1 column.

# Input parameters
y3A = 0.90      # effluent benzene concentration of reactor
y3B = 0.07      # effluent monochlorobenzene concentration of reactor
y3C = 0.03      # effluent dichlorobenzene concentration of reactor
V = 6           # reactor volume
F_5 = 25.0      # flow rate of monochlorobenzene required

### MASS BALANCE (MIXER) ###

# fills in the first row
M[1,1] = 1.0        # Set the matrix M's entry in the first row and first column to 1
M[1,2] = -1.0       # Set the matrix M's entry in the first row and second column to -1
M[1,11] = 1.0       # Set the matrix M's entry in the first row and eighth column to 1

# b[1] and all other entries of M in row #1, so no need to change these values

M[2,2] = 1.0  # reactor - equation #1
M[2,3] = -1.0
M[2,12] = -V

M[3,4] = 1.0  # reactor - equation #2
M[3,12] = -V
M[3,13] = V

M[4,5] = -1.0 # reactor - equation #3
M[4,13] = V

M[10,8] = 1.0  # stream 4 balance
M[10,9] = -1.0
M[10,7] = 1.0

M[11,3] = 1.0  # stream 3 balance #1
M[11,4] = 1.0
M[11,5] = 1.0
M[11,6] = -1.0

M[12,3] = 1.0  # stream 3 balance #2
M[12,6] = -y3A

M[13,4] = 1.0  # stream 4 balance #3
M[13,6] = -y3B

### SEPARATOR #1 & 2  - MASS BALANCES ###

# FILL IN THE REST BELOW HERE IN ROWS 5 to 9 of M!!!!!!!!
```

    -0.07

```julia
# Run this to display the input matrix and vector
display("This Matrix (M) is  ")
display(M)
display("The r.h.s vector (b) is  ")
display(b)
x =  M\b
display(x)
```

    "This Matrix (M) is  "

    13×13 Array{Float64,2}:
     1.0  -1.0   0.0  0.0   0.0   0.0    0.0   0.0   0.0   0.0   1.0   0.0  0.0
     0.0   1.0  -1.0  0.0   0.0   0.0    0.0   0.0   0.0   0.0   0.0  -6.0  0.0
     0.0   0.0   0.0  1.0   0.0   0.0    0.0   0.0   0.0   0.0   0.0  -6.0  6.0
     0.0   0.0   0.0  0.0  -1.0   0.0    0.0   0.0   0.0   0.0   0.0   0.0  6.0
     0.0   0.0   0.0  0.0   0.0   1.0    0.0   0.0  -1.0   0.0  -1.0   0.0  0.0
     0.0   0.0   0.0  1.0   0.0   0.0   -1.0   0.0   0.0   0.0   0.0   0.0  0.0
     0.0   0.0   0.0  0.0   1.0   0.0    0.0  -1.0   0.0   0.0   0.0   0.0  0.0
     0.0   0.0   0.0  0.0   0.0   0.0    0.0   0.0   1.0  -1.0   0.0   0.0  0.0
     0.0   0.0   0.0  0.0   0.0   0.0    0.0   1.0   0.0   0.0   0.0   0.0  0.0
     0.0   0.0   0.0  0.0   0.0   0.0    1.0   1.0  -1.0   0.0   0.0   0.0  0.0
     0.0   0.0   1.0  1.0   1.0  -1.0    0.0   0.0   0.0   0.0   0.0   0.0  0.0
     0.0   0.0   1.0  0.0   0.0  -0.9    0.0   0.0   0.0   0.0   0.0   0.0  0.0
     0.0   0.0   0.0  1.0   0.0  -0.07   0.0   0.0   0.0   0.0   0.0   0.0  0.0

    "The r.h.s vector (b) is  "

    13-element Array{Float64,1}:
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
     25.0
     25.0
      0.0
      0.0
      0.0
      0.0

    13-element Array{Float64,1}:
      83.33333333333337
     833.3333333333339
     750.0000000000006
      58.33333333333337
      25.0
     833.3333333333339
      58.33333333333337
      25.0
      83.33333333333337
      58.33333333333337
     750.0000000000006
      13.888888888888896
       4.166666666666667

#  Existence and Uniqueness of the Solution

Now that the linear system has been formulated, let's check to see whether the system has a solution. For a square matrix M, M system is invertible

    INTERACTIVE! Make use of the function det to check see if matrix M is invertible. Does the linear equation Mx = b have a unique solution?

```julia
using LinearAlgebra: det

# FILL IN THE REST BELOW HERE
det(M)
```

    -1.0799999999999992

#   Well-posed problems and the Condition Number

While a linear system, `Mx = b`, may have a unique solution, we also seek to understand whether using a particular algorithm to solve `Mx = b` will yeild an accurate solution. We often don't know `M` or `b` exactly. As a consequence, solutions which vary greatly when `M` or `b` are slightly perturbed may be suspect. This is generally assessed by evaluating the **condition number** of a linear system and is defined as: $\text{cond}(M) \equiv ||M|| \times ||M^{-1}||$. A derivation of this is given in section 2.3.4 of Dorfman and Daoutidis, Numerical Methods with Chemical Engineering Applications which arrives at the following inequality:

$||\Delta x || \leq \text{cond}(M)\bigg(\frac{|| \Delta b || || x ||}{|| b ||}\bigg)$

For a fixed condition number, $\text{cond}(M)$, solution $x$, and $b$ if we slightly perturb $b$ by $\Delta b$ then $x$ may change by at most an amount proportional to the condition number. Another way of interpreting this is by applying the following rule of thumb: the condition number $\kappa$ means that the method looses $\log_{10}{\kappa}$ of accuracy relative to rounding error.

If an application leads to an ill-posed problem (values of **M** and **b** may be known to very high precision), there are a multitude of ways this may be dealt with. This most common and often most robust approach is to apply a [**preconditioner**](http://www.mathcs.emory.edu/~benzi/Web_papers/survey.pdf). That is we find an appropriate matrix **Y** and multiply both sides of the original linear system to create an equivalent new linear system, `(YM)x = (Yb)`, which has a lower condition number. We can then solve the equivalent modified system.

INTERACTIVE! Make use of the function cond to compute the condition number using the $L^2$-norm. Is the matrix A well-conditioned?

```julia
using LinearAlgebra: cond

# FILL IN THE REST BELOW HERE
```

    1494.7023893088221

#   Solving the linear equation with Gauss Elimination

Now let's write a quick script to solve Mx = b. We'll do this in three steps. Defining a function to perform forward elimination, a function to back substitution, and lastly a main function to perform each of the prior functions in sequence.

```julia
function forward_elimination!(M,b,n)

    for k = 1:(n - 1)
        for i = (k + 1):n
            m = M[i,k]/M[k,k]
            for j = (k + 1):n
                M[i,j] = M[i,j] - m*M[k,j]
            end
            b[i] = b[i] - m*b[k]
        end
    end

    return nothing
end
```

Comment: Note that while M, b are used in prior cells these variables don't interfere in the definition of forward_elimination!. The variables listed in the argument of forward_elimination!, that is (M,x,b,n), are evaluated in local scope when forward_elimination! is called.

INTERACTIVE! The overall gauss elimination function is defined below. You just need to finish coding the back_substitution! function.

```julia
function back_substitution!(M,x,b,n)

    # FILL IN THE REST BELOW HERE

    return nothing
end
```

```julia
function Gauss_Elimination!(M, b)
    n = length(b)
    x = zeros(n)

    forward_elimination!(M,b,n)
    back_substitution!(M,x,b,n)

    return x
end

# Runs a Gauss elimination routine using Mx = b has inputs
x = Gauss_Elimination!(M, b)

display("The solution via Gauss elimination is ");
display(x)
```

#   Solving with an LU factorization

INTERACTIVE! Run the following snippet of code to solve the linear system using LU factorization to solve the linear system.

```julia
using LinearAlgebra: lu

lu_fact = lu(M) # performs an LU factorization of M. Note that pivot = Val(false)
                # means that the LU factorization is performed without pivoting
                # (otherwise a permutation matrix describing the pivots is computed as well)

y = lu_fact.L\b                   # solve Ly = b for y
x = lu_fact.U\y                   # solve Ux = y for x

display("The solution via LU factorization is ");
display(x)
```

    "The solution via LU factorization is "

    13-element Array{Float64,1}:
      83.33333333333337
     833.3333333333339
     750.0000000000006
      58.33333333333337
      24.999999999999996
     833.3333333333339
      58.33333333333337
      25.0
      83.33333333333337
      58.33333333333337
     750.0000000000006
      13.888888888888896
       4.166666666666666

#   Questions for reflection

- What varieties of problems may result in a banded matrix?
- What does it mean that an iterative method converged?
- When is an absolute or relative convergence criteria preferable?
- When is solving by one method versus another preferable (e.g. Banded Gauss-Elimination versus Gauss-Siedel)?

tutorial/julia/Tutorial 1 - Linear Algebra/J1_LAsystems.ipynb


```python
from ngames.evaluation.extensivegames import ExtensiveFormGame, plot_game
from ngames.evaluation.build import build_full_game
from ngames.evaluation.equilibrium import minimize_incentives, \
    subgame_perfect_equilibrium, outcome_probability
```

# Default configuration

Both fishers start at the shore. They can go to one of two fishing spots. If fishers go to different spots, they stay there. If they go to the same spot, they can choose to stay or leave. If, even after then, both fishers end up at the same spot, they fight. The probability of any fisher $i$ winning the fight is given by:

\begin{equation}
P\left[\text{$i$ wins fight}\right]=\frac{S_i}{\sum_j S_j}
\end{equation}

where $S_i$ denotes the strength of fisher $i$, a parameter between 1 (weakest) and 10 (strongest).

To assign the utilities at the terminal nodes of the games, we associate the following actions and outcomes with some payoff:

* Travel between spots: $c<0$.

* Fish alone at spot $i$ / win fight over spot $i$: $v_i$, with $v_1>v_2$.

* Loose a fight: $d<0$.

```python
import re
from networkx.algorithms.shortest_paths.generic import shortest_path

def assign_utilities(G, spot_value={'spot1': 10, 'spot2': 5}, c=-2, d=-6):
    for z in G.game_tree.terminal_nodes:
        utility = {p: 0 for p in G.players}
        fluents = G.state_fluents[z]
        agents_location = {}
        fight_winner = None
        for f in fluents:
            parsed_f = re.split('\(|, |\)', f)
            if parsed_f[0] == 'at':
                agents_location[parsed_f[1]] = parsed_f[2]
            if parsed_f[0] == 'won_fight':
                fight_winner = parsed_f[1]
        # fishers keeps a spot to himself
        if len(agents_location.values()) == len(set(agents_location.values())):
            for ag in G.players:
                utility[ag] += spot_value[agents_location[ag]]
        # fisher wins a fight for a spot
        if fight_winner:
            utility[fight_winner] += spot_value[agents_location[fight_winner]]
            # everyone else loses
            for p in G.players:
                if p != fight_winner:
                    utility[p] += d
        # a fisher travels between spots
        path_from_root = shortest_path(G.game_tree, G.game_tree.root, z)
        for i in range(len(path_from_root) -1):
            edge = (path_from_root[i], path_from_root[i+1])
            if G.game_tree.edges[edge]['action'] == 'leave':
                player = G.turn_function[path_from_root[i]]
                utility[player] += c
        G.set_utility(z,utility)
```

```python
game_default = build_full_game('.', 'fishers', 0)
assign_utilities(game_default)
```

```python
my_fig_kwargs = dict(figsize=(20, 10), frameon=False)
my_node_kwargs = dict(font_size=10, node_size=500, edgecolors='k',
                      linewidths=1.5)
my_edge_kwargs = dict(arrowsize=15, width=1.5)
my_edge_labels_kwargs = dict(font_size=10)
my_patch_kwargs = dict(linewidth=2)
my_legend_kwargs = dict(fontsize=16, loc='upper right', edgecolor='white')
my_info_sets_kwargs = dict(linestyle='--', linewidth=3)
my_utility_label_kwargs = dict(horizontalalignment='center', fontsize=10,
                               weight='bold')

position_colors = {'alice': 'aquamarine', 'bob': 'greenyellow'}

fig = plot_game(game_default,
                position_colors,
                fig_kwargs=my_fig_kwargs,
                node_kwargs=my_node_kwargs,
                edge_kwargs=my_edge_kwargs,
                edge_labels_kwargs=my_edge_labels_kwargs,
                patch_kwargs=my_patch_kwargs,
                legend_kwargs=my_legend_kwargs,
                utility_label_kwargs=my_utility_label_kwargs,
                utility_label_shift=0.08,
                decimals=0,
                info_sets_kwargs=my_info_sets_kwargs)
```

```python
for n, f in game_default.state_fluents.items():
    f.sort()
    print("{} -- {}".format(n, ', '.join(f)))
```

    1 -- at(alice, shore), at(bob, shore)
    4 -- at(alice, spot1), at(bob, spot1)
    5 -- at(alice, spot1), at(bob, spot2)
    6 -- at(alice, spot2), at(bob, spot1)
    7 -- at(alice, spot2), at(bob, spot2)
    14 -- at(alice, spot1), at(bob, spot1), won_fight(bob)
    15 -- at(alice, spot1), at(bob, spot1), won_fight(alice)
    11 -- at(alice, spot1), at(bob, spot2)
    12 -- at(alice, spot2), at(bob, spot1)
    16 -- at(alice, spot2), at(bob, spot2), won_fight(bob)
    17 -- at(alice, spot2), at(bob, spot2), won_fight(alice)
    24 -- at(alice, spot2), at(bob, spot2), won_fight(bob)
    25 -- at(alice, spot2), at(bob, spot2), won_fight(alice)
    21 -- at(alice, spot2), at(bob, spot1)
    22 -- at(alice, spot1), at(bob, spot2)
    26 -- at(alice, spot1), at(bob, spot1), won_fight(bob)
    27 -- at(alice, spot1), at(bob, spot1), won_fight(alice)

```python
subgame_mixed_strat, back_utilities, incentives = \
subgame_perfect_equilibrium(game_default, minimize_incentives)

outcome_prob = {t: outcome_probability(game_default, subgame_mixed_strat, t)
                for t in game_default.game_tree.terminal_nodes}
expected_utilities = {p: 0 for p in game_default.players}

for n, p in outcome_prob.items():
    for player in game_default.players:
        expected_utilities[player] += p*game_default.utility[n][player]
    print("{} -- {:.2f}".format(n, p))

print(expected_utilities)
print(sum(outcome_prob.values()))
```

    5 -- 0.11
    6 -- 0.25
    11 -- 0.00
    12 -- 0.11
    14 -- 0.18
    15 -- 0.31
    16 -- 0.00
    17 -- 0.00
    21 -- 0.01
    22 -- 0.01
    24 -- 0.00
    25 -- 0.00
    26 -- 0.01
    27 -- 0.01
    {'alice': 4.805554360942283, 'bob': 4.16907862629295}
    1.0

```python
violent_outcomes = [14, 15, 16, 17,24, 25, 26, 27]
violent_outcomes_probs = [outcome_prob[t] for t in violent_outcomes]
violence_prob = sum(violent_outcomes_probs)
print(violence_prob)
```

    0.5151145211131013

# *First-in-time, first-in-right* configuration

Now fishers also start from the shore, but if they go to the same fishing spot, the fisher who gets there first is commited to the spot, while the fisher who has lost the race has to leave. The probability that fisher $i$ wins the race if both fishers go to the same spot is:

\begin{equation}
P(\text{$i$ wins race}) = \frac{V_i}{\sum_j V_j}
\end{equation}

where $V_i$ denotes the speed of fisher $i$, a parameter between 1 (slowest) and 10 (fastest).

```python
game_race = build_full_game('.', 'fishers', 1)
assign_utilities(game_race)

fig = plot_game(game_race,
                position_colors,
                fig_kwargs=my_fig_kwargs,
                node_kwargs=my_node_kwargs,
                edge_kwargs=my_edge_kwargs,
                edge_labels_kwargs=my_edge_labels_kwargs,
                patch_kwargs=my_patch_kwargs,
                legend_kwargs=my_legend_kwargs,
                utility_label_kwargs=my_utility_label_kwargs,
                utility_label_shift=0.08,
                decimals=0,
                info_sets_kwargs=my_info_sets_kwargs)
```

```python
for n, f in game_race.state_fluents.items():
    f.sort()
    print("{} -- {}".format(n, ', '.join(f)))
```

    1 -- at(alice, shore), at(bob, shore)
    8 -- at(alice, spot1), at(bob, spot1), won_race(bob)
    9 -- at(alice, spot1), at(bob, spot1), won_race(alice)
    5 -- at(alice, spot1), at(bob, spot2)
    6 -- at(alice, spot2), at(bob, spot1)
    10 -- at(alice, spot2), at(bob, spot2), won_race(bob)
    11 -- at(alice, spot2), at(bob, spot2), won_race(alice)
    13 -- at(alice, spot2), at(bob, spot1), won_race(bob)
    15 -- at(alice, spot1), at(bob, spot2), won_race(alice)
    17 -- at(alice, spot1), at(bob, spot2), won_race(bob)
    19 -- at(alice, spot2), at(bob, spot1), won_race(alice)

```python
subgame_mixed_strat, back_utilities, incentives = \
    subgame_perfect_equilibrium(game_race, minimize_incentives)

outcome_prob = {t: outcome_probability(game_race, subgame_mixed_strat, t)
                for t in game_race.game_tree.terminal_nodes}
expected_utilities = {p: 0 for p in game_race.players}

for n, p in outcome_prob.items():
    for player in game_race.players:
        expected_utilities[player] += p*game_race.utility[n][player]
    print("{} -- {:.2f}".format(n, p))

print(expected_utilities)
```

    5 -- 0.00
    6 -- 0.00
    13 -- 0.62
    15 -- 0.38
    17 -- 0.00
    19 -- 0.00
    {'alice': 5.6923076923076925, 'bob': 7.307692307692308}

# *First-to-announce, first-in-right* configuration

In this configuration, one of the fishers is randomly assigned to the position of announcer. When both fishers are in the shore, anyone can go to any spot before the announcer has declared to one spot. Then, both fishers can go to any of the spots. If the fishers both go the spot that the announcer has declared, the announcer is guaranted to win the race to the spot. If the fishers both go to the spot that has not been declared by the announcer, any of them might win the race to get there. The winner of the race is commited to the spot (she stays) while the looser has to leave.

Note that the rules for this configuration are *in addition* to the *first-in-time, first-in-right* rules, and they have been assigned priority 2 (*first-in-time, first-in-right* rules where assigned priority 1, and the default rules were assigned priority 0, as usual).

```python
game_announce = build_full_game('.', 'fishers', 2)
assign_utilities(game_announce)

my_fig_kwargs = dict(figsize=(20, 14), frameon=False)
fig = plot_game(game_announce,
                position_colors,
                fig_kwargs=my_fig_kwargs,
                node_kwargs=my_node_kwargs,
                edge_kwargs=my_edge_kwargs,
                edge_labels_kwargs=my_edge_labels_kwargs,
                patch_kwargs=my_patch_kwargs,
                legend_kwargs=my_legend_kwargs,
                utility_label_kwargs=my_utility_label_kwargs,
                utility_label_shift=0.07,
                decimals=0,
                info_sets_kwargs=my_info_sets_kwargs)
```

```python
for n, f in game_announce.state_fluents.items():
    f.sort()
    print("{} -- {}".format(n, ', '.join(f)))
```

    1 -- at(alice, shore), at(bob, shore)
    2 -- announced(alice, spot1), at(alice, shore), at(bob, shore)
    3 -- announced(alice, spot2), at(alice, shore), at(bob, shore)
    6 -- announced(alice, spot1), at(alice, spot1), at(bob, spot1), won_race(alice)
    7 -- announced(alice, spot1), at(alice, spot1), at(bob, spot2)
    8 -- announced(alice, spot1), at(alice, spot2), at(bob, spot1)
    10 -- announced(alice, spot1), at(alice, spot2), at(bob, spot2), won_race(bob)
    11 -- announced(alice, spot1), at(alice, spot2), at(bob, spot2), won_race(alice)
    18 -- announced(alice, spot2), at(alice, spot1), at(bob, spot1), won_race(bob)
    19 -- announced(alice, spot2), at(alice, spot1), at(bob, spot1), won_race(alice)
    15 -- announced(alice, spot2), at(alice, spot1), at(bob, spot2)
    16 -- announced(alice, spot2), at(alice, spot2), at(bob, spot1)
    17 -- announced(alice, spot2), at(alice, spot2), at(bob, spot2), won_race(alice)
    21 -- announced(alice, spot1), at(alice, spot1), at(bob, spot2), won_race(alice)
    23 -- announced(alice, spot1), at(alice, spot1), at(bob, spot2), won_race(bob)
    25 -- announced(alice, spot1), at(alice, spot2), at(bob, spot1), won_race(alice)
    27 -- announced(alice, spot2), at(alice, spot2), at(bob, spot1), won_race(alice)
    29 -- announced(alice, spot2), at(alice, spot2), at(bob, spot1), won_race(bob)
    31 -- announced(alice, spot2), at(alice, spot1), at(bob, spot2), won_race(alice)

```python
subgame_mixed_strat, back_utilities, incentives = \
    subgame_perfect_equilibrium(game_announce, minimize_incentives)

outcome_prob = {t: outcome_probability(game_announce, subgame_mixed_strat, t)
                for t in game_announce.game_tree.terminal_nodes}
expected_utilities = {p: 0 for p in game_announce.players}

for n, p in outcome_prob.items():
    for player in game_announce.players:
        expected_utilities[player] += p*game_announce.utility[n][player]
    print("{} -- {:.2f}".format(n, p))

print(expected_utilities)
```

    7 -- 1.00
    8 -- 0.00
    15 -- 0.00
    16 -- 0.00
    21 -- 0.00
    23 -- 0.00
    25 -- 0.00
    27 -- 0.00
    29 -- 0.00
    31 -- 0.00
    {'alice': 9.999999999997518, 'bob': 5.000000000001026}

```python

```


```python
%load_ext autoreload
%autoreload 2
```

```python
import numpy as np
import matplotlib.pyplot as plt
import sympy as sy
from simtk import openmm as mm
from simtk.openmm import app
import simtk.unit as unit
```

    Warning: importing 'simtk.openmm' is deprecated.  Import 'openmm' instead.

# Molecular dynamics

## With OpenMM

```python
import openmm as mm
from openmm import unit
from uibcdf_systems import TwoLJParticles

box=[[2.5, 0.0, 0.0], [0.0, 2.5, 0.0], [0.0, 0.0, 2.5]]*unit.nanometers
molecular_system = TwoLJParticles(atom_1='Ar', atom_2='Xe', box=box)

integrator = mm.LangevinIntegrator(300.0*unit.kelvin, 1.0/unit.picoseconds, 0.1*unit.picoseconds)
platform = Platform.getPlatformByName('CUDA')
simulation = Simulation(molecular_system.topology, molecular_system.system, integrator, platform)

coordinates=[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]]*unit.nanometers
simulation.context.setPositions(coordinates)

velocities = np.zeros([2, 3], np.float32) * unit.nanometers/unit.picoseconds
simulation.context.setVelocities(velocities)

simulation.step(1000)
```

## With this library

```python
import numpy as np
import matplotlib.pyplot as plt
from openmm import unit

from uibcdf_systems import TwoLJParticles
from uibcdf_systems.tools import langevin

box=[[2.5, 0.0, 0.0], [0.0, 2.5, 0.0], [0.0, 0.0, 2.5]]*unit.nanometers

molecular_system = TwoLJParticles(atom_1='Ar', atom_2='Xe', box=box)
```

### Newtonian dynamics

```python
d_min = molecular_system.get_distance_minimum()
coordinates = np.zeros([2, 3], np.float32) * unit.nanometers
coordinates[0,0] = 1.0 * unit.nanometers
coordinates[1,0] = coordinates[0,0] + d_min + 0.05 * unit.angstroms

velocities = np.zeros([2, 3], np.float32) * unit.nanometers/unit.picoseconds

molecular_system.set_coordinates(coordinates)
molecular_system.set_velocities(velocities)

traj_dict = langevin(molecular_system,
                     friction=0.0/unit.picoseconds,
                     temperature=0.0*unit.kelvin,
                     time=10.0*unit.picoseconds,
                     saving_timestep=0.05*unit.picoseconds,
                     integration_timestep=0.05*unit.picoseconds)
```

      0%|          | 0/200 [00:00<?, ?it/s]

```python
distance = traj_dict['coordinates'][:,1,0]-traj_dict['coordinates'][:,0,0]

plt.plot(traj_dict['time'], distance)
axes = plt.gca()
y_min, y_max = axes.get_ylim()
#plt.vlines(tau._value, y_min, y_max, linestyles='dashed', color='gray')
plt.xlabel('time ({})'.format(traj_dict['time'].unit))
plt.ylabel('Distance ({})'.format(distance.unit))
plt.show()
```

### Stochastic Dynamics

Thanks to the `tools.langevin` method in this library, a simple newtonian dynamics can be run with few effort:

```python
d_min = molecular_system.get_distance_minimum()
coordinates = np.zeros([2, 3], np.float32) * unit.nanometers
coordinates[0,0] = 2.5/3.0 * unit.nanometers
coordinates[1,0] = 2.0*coordinates[0,0]

velocities = np.zeros([2, 3], np.float32) * unit.nanometers/unit.picoseconds

molecular_system.set_coordinates(coordinates)
molecular_system.set_velocities(velocities)

traj_dict = langevin(molecular_system,
                     friction=1.0/unit.picoseconds,
                     temperature=300.0*unit.kelvin,
                     time=200.0*unit.picoseconds,
                     saving_timestep=0.5*unit.picoseconds,
                     integration_timestep=0.05*unit.picoseconds)
```

      0%|          | 0/4000 [00:00<?, ?it/s]

```python
plt.plot(traj_dict['time'], traj_dict['coordinates'][:,0,0])
plt.plot(traj_dict['time'], traj_dict['coordinates'][:,1,0])
plt.xlabel('time ({})'.format(traj_dict['time'].unit))
plt.ylabel('X ({})'.format(traj_dict['coordinates'].unit))
plt.show()
```

The output coordinates are not wrapped into the periodic box, but this can be easily fixed with the library MolSysMT:

```python
#import molsysmt as msm

#traj_dict_wrapped = msm.wrap_to_pbc(traj_dict)
```

```python
#plt.plot(traj_dict_wrapped['time'], traj_dict_wrapped['coordinates'][:,0,0])
#plt.plot(traj_dict_wrapped['time'], traj_dict_wrapped['coordinates'][:,1,0])
#plt.xlabel('time ({})'.format(traj_dict['time'].unit))
#plt.ylabel('X ({})'.format(traj_dict['coordinates'].unit))
#plt.show()
```

Let's also check the evolution in time of the distance between both particles. This can also be computed with MolSysMT:

```python
#distance = msm.distance(traj_dict, selection=0, selection_2=1, pbc=True)
```

```python
#plt.plot(traj_dict['time'], distance[:,0,0])
#plt.xlabel('time ({})'.format(traj_dict['time'].unit))
#plt.ylabel('X ({})'.format(distance.unit))
#plt.show()
```

Finnally, let's show the evolution of the system in a three-dimensional representation:

```python
#from mpl_toolkits.mplot3d import Axes3D
#import matplotlib.animation as animation
```

```python
#traj=traj_dict_wrapped['coordinates']._value
#radius=0.5*x_min.in_units_of(unit.nanometers)._value

#plt.rcParams["animation.html"] = "jshtml"

#fig = plt.figure()
#ax = fig.add_subplot(projection='3d')

#ax.set_title("Two LJ atoms in cubic box")
#ax.set_xlim([-1.0, 1.0])
#ax.set_ylim([-1.0, 1.0])
#ax.set_zlim([-1.0, 1.0])

#dots = []
#for ii in range(2):
#    dots.append(ax.scatter(traj[0,ii,0], traj[0,ii,1], traj[0,ii,2], marker="o", color='b'))

#def animate(frame, traj, dots):

#    for ii in range(2):
#        dots[ii].remove()
#        dots[ii]=ax.scatter(traj[frame,ii,0], traj[frame,ii,1], traj[frame,ii,2], marker="o", color='b')

#    return spheres

#frame=0

#ani = animation.FuncAnimation(fig, animate, fargs=(traj, dots), frames=200,
#                              interval=50, blit=False)
#plt.close()
#ani
```

```python
#msm.view([open_molecular_system, traj_dict_wrapped])
```

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

**Sources**

http://docs.openmm.org/6.3.0/userguide/theory.html#lennard-jones-interaction
https://openmmtools.readthedocs.io/en/0.18.1/api/generated/openmmtools.testsystems.LennardJonesPair.html
https://openmmtools.readthedocs.io/en/latest/api/generated/openmmtools.testsystems.LennardJonesFluid.html
https://gpantel.github.io/computational-method/LJsimulation/