Read two saved template Models
Interpolate Ge and Ph data to create input and output targets for training.

1. Load existing template NN models
2. Create parameters for generating training data from source template models
3. Initialize template interpolation (ti) model
4. Train ti model
5. Generate test data for graphing
6. Save ti model to file
7. Generate blender data from ti model output

```python
import torch as torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

import os
import numpy as np
import json
import math as m
import random
import time
import NNArt as gy
import NNArt1 as gy1
from sympy.utilities.iterables import multiset_permutations

import plotly as py
import plotly.offline as offline
import plotly.graph_objs as go
import plotly.figure_factory as ff
import plotly.io as pio

CURRENT_MODEL_SAVE_DIR = './save/'

offline.init_notebook_mode(connected=True)
np.set_printoptions(precision=3, suppress=True)
```

```python
class Net(nn.Module):
    def __init__(self, layerSizes, name):
        super(Net, self).__init__()
        self.shape = layerSizes
        self.name = name
        self.seq = nn.Sequential(
            nn.Linear(layerSizes[0],layerSizes[1]),
            nn.ELU(),
            nn.Linear(layerSizes[1],layerSizes[2]),
            nn.ELU(),
            nn.Linear(layerSizes[2],layerSizes[3]),
            nn.ELU()
        )
    def forward(self, x):
        return self.seq(x)

class NetEEL(nn.Module):
    def __init__(self, layerSizes, name):
        super(NetEEL, self).__init__()
        self.shape = layerSizes
        self.name = name
        self.seq = nn.Sequential(
            nn.Linear(layerSizes[0],layerSizes[1]),
            nn.ELU(),
            nn.Linear(layerSizes[1],layerSizes[2]),
            nn.ELU(),
            nn.Linear(layerSizes[2],layerSizes[3]),
            nn.ELU(),

        )
    def forward(self, x):
        return self.seq(x)
```

```python
'''
1. Load existing template NN models
templateModel2

'''

tmModel0 = Net([2,10,15,21],'tmModel0')
tmModel1 = Net([2,10,15,21],'tmModel1')
relModel = Net([21,15,10,3],'relModel')
genModel = Net([3,10,15,21],'genModel')
cohModel = Net([3,5,3,2],'cohModel')

PATH=CURRENT_MODEL_SAVE_DIR+'templateModel0.pth'
tmModel0.load_state_dict(torch.load(PATH))
PATH=CURRENT_MODEL_SAVE_DIR+'templateModel1.pth'
tmModel1.load_state_dict(torch.load(PATH))
PATH=CURRENT_MODEL_SAVE_DIR+'REL PhRc-T0T1-mask 11p05.pth'
relModel.load_state_dict(torch.load(PATH))
PATH=CURRENT_MODEL_SAVE_DIR+'GEN RcGen-T0T1-mask 11p05.pth'
genModel.load_state_dict(torch.load(PATH))
PATH=CURRENT_MODEL_SAVE_DIR+'cohModel.pth'
cohModel.load_state_dict(torch.load(PATH))
tmModel0.eval()
tmModel1.eval()
relModel.eval()
genModel.eval()
cohModel.eval()

tmModel0.shape, tmModel1.shape,genModel.shape
```

    ([2, 10, 15, 21], [2, 10, 15, 21], [3, 10, 15, 21])

```python
'''
4. Review Emergent Context

'''
morphIndices=[0.,0.1,.2,.3,.5,.7,.9,1.0, 1.1]
timeIndices=[0.,0.1,.2,.3,.5,.7,.9,1.0, 1.1]

#sample source templates to get ph samples
relIn = gy.createRmInput(morphIndices, timeIndices, sPermutation=[])
tmOut0 = tmModel0(torch.Tensor(relIn))
tmOut1 = tmModel1(torch.Tensor(relIn))

#use template generated ph samples to feed through contex models
relCout0 = relModel(tmOut0)
relCout1 = relModel(tmOut1)

genOut0 = genModel(relCout0)
genOut1 = genModel(relCout1)

cohOut0 = cohModel(relCout0)
cohOut1 = cohModel(relCout1)

relIn.shape, tmOut0.shape, relCout0.shape, genOut0.shape, cohOut0.shape
```

    ((9, 9, 2),
     torch.Size([9, 9, 21]),
     torch.Size([9, 9, 3]),
     torch.Size([9, 9, 21]),
     torch.Size([9, 9, 2]))

```python
'''
Build xy values for
Context values over morphology for different time values

'''

#relCout0 for 0-8 time intervals
gTp0T0 = np.array([relIn[0,:,1],relCout0[0,:,2].detach().numpy()])
gTp0Tp2 = np.array([relIn[2,:,1],relCout0[2,:,2].detach().numpy()])
gTp0Tp5 = np.array([relIn[4,:,1],relCout0[4,:,2].detach().numpy()])
gTp0Tp9 = np.array([relIn[6,:,1],relCout0[6,:,2].detach().numpy()])
gTp0T1 = np.array([relIn[7,:,1],relCout0[7,:,2].detach().numpy()])
gTp0T1p1 = np.array([relIn[8,:,1],relCout0[8,:,2].detach().numpy()])

#relCout1 for 0-8 time intervals
gTp1T0 = np.array([relIn[0,:,1],relCout1[0,:,2].detach().numpy()])
gTp1Tp2 = np.array([relIn[2,:,1],relCout1[2,:,2].detach().numpy()])
gTp1Tp5 = np.array([relIn[4,:,1],relCout1[4,:,2].detach().numpy()])
gTp1Tp9 = np.array([relIn[6,:,1],relCout1[6,:,2].detach().numpy()])
gTp1T1 = np.array([relIn[7,:,1],relCout1[7,:,2].detach().numpy()])
gTp1T1p1 = np.array([relIn[8,:,1],relCout1[8,:,2].detach().numpy()])

gx = [gTp0T0, gTp1T0, gTp0Tp2, gTp1Tp2, gTp0Tp5, gTp1Tp5,
    gTp0Tp9, gTp1Tp9,gTp0T1p1,gTp1T1p1]

```

```python
layout = go.Layout(
    title = 'Initial Curves',
    height = 700,
    width = 800,
    hovermode= 'closest',
    xaxis= dict(
        title= 'Morphology',
        range = [-.1,1.5],
    ),
    yaxis=dict(
        title= 'Context',
        range = [-.6,1.],
    )
)
layout.title = 'Context values over morphology for different time values'
f = gy.graphCurves(gx, layout, labels=['gTp0T0','gTp1T0', 'gTp0Tp2', 'gTp1Tp2', 'gTp0T0p5', 'gTp1T0p5',
                                       'gTp0T0p9','gTp1T0p9',  'gTp0T0_1p1','gTp1T0_1p1'],
                   dashRange=gy.rangeList([range(2,4),range(6,8)]))

offline.iplot(f)

```

```python
'''
Format for graphing at a time point

relIn[t,:,1]    is X axis m values,
relCout0[t,:,2] is y axis is context values for 0 template

'''

t=8
#structure context values for graphing
con0 = np.array([relIn[t,:,1],relCout0[t,:,2].data.numpy()])
con1 = np.array([relIn[t,:,1],relCout1[t,:,2].data.numpy()])

#coherence values
#reshape into a 1,1,9 list of 9 coh values
# ch0 = np.reshape(cohOut0[t].data.numpy(), [1,2,-1])
# ch1 = np.reshape(cohOut1[t].data.numpy(), [1,2,-1])
ch0 = np.reshape(cohOut1[t].data.numpy()[:,0],[1,1,-1])
coh0 = np.array([relIn[t,:,1],ch0[0,0]])
cohD0 =  np.array(con0)
cohD0[1,:] = coh0[1,:]+con0[1,:]
```

```python
# '''
# Format for graphing at a time point

# relIn[t,:,1]    is X axis m values,
# relCout0[t,:,2] is y axis is context values for 0 template

# '''

# t=8
# #structure context values for graphing
# con0 = np.array([relIn[t,:,1],relCout0[t,:,2].data.numpy()])
# con1 = np.array([relIn[t,:,1],relCout1[t,:,2].data.numpy()])

# #coherence values
# #reshape into a 1,1,9 list of 9 coh values
# ch0 = np.reshape(cohOut0[t].data.numpy(), [1,1,-1])
# ch1 = np.reshape(cohOut1[t].data.numpy(), [1,1,-1])
# #add m values for x axis
# coh0 = np.array([relIn[t,:,1],ch0[0,0]])
# coh1 = np.array([relIn[t,:,1],ch1[0,0]])

# #cohD adds coh t0 context
# cohD0 =  np.array(con0)
# cohD1 =  np.array(con1)
# cohD0[1,:] = coh0[1,:]+con0[1,:]
# cohD1[1,:] = coh1[1,:]+con1[1,:]

```

```python
gxx = [con0,con1,coh0,cohD0]
layout = go.Layout(
    title = 'Initial Curves',
    height = 600,
    width = 600,
    hovermode= 'closest',
    xaxis= dict(
        title= 'X',
        range = [-.4,1.2],
    ),
    yaxis=dict(
        title= 'Y',
        range = [-.4,.5],
    )
)
fxx = gy.graphCurves(gxx, layout, labels=['context0','context1','coherence0','coherence1', 'con0+coh0','con1+coh1'], dashRange=range(0,2))
offline.iplot(fxx)
```

```python
#create a matrix of mesh values
mm,cc = np.meshgrid(np.arange(-0., 1.1, .1), np.arange(-.5, .9, .1))

t = .9
ma = np.array(mm)
ca = np.array(cc)
xList = []
yList = []
uList = []
vList = []
for mi in range(ma.shape[0]):
    for ci in range(ca.shape[1]):
        cIn = torch.Tensor([[t,ma[mi,ci], ca[mi,ci]]])
        cohOut = cohModel(cIn)
        x = ma[mi,ci]
        y = ca[mi,ci]
        u = 0
        v = cohOut.data.numpy()[0,0] #extract value 0.0716
        xList.append(x)
        yList.append(y)
        uList.append(u)
        vList.append(v)

xx = np.array(xList)
yy = np.array(yList)
uu = np.array(uList)
vv = np.array(vList)

fig0 = ff.create_quiver(xx, yy, uu, vv,
       scale=.4,
       arrow_scale=.4,
       name='quiver'
       #,line=dict(width=2)
      )

fig0.layout.title = 'Coherence field for time ' + str(t)
fig0.add_trace(f.data[6])
fig0.add_trace(f.data[7])
offline.iplot(fig0)
layout.title
```

    layout.Title({
        'text': 'Initial Curves'
    })

```python
#create a matrix of mesh values
mm,cc = np.meshgrid(np.arange(-0., 1.1, .1), np.arange(-.5, .9, .1))

t = .5
ma = np.array(mm)
ca = np.array(cc)
xList = []
yList = []
uList = []
vList = []
for mi in range(ma.shape[0]):
    for ci in range(ca.shape[1]):
        cIn = torch.Tensor([[t,ma[mi,ci], ca[mi,ci]]])
        cohOut = cohModel(cIn)
        x = ma[mi,ci]
        y = ca[mi,ci]
        u = 0
        v = cohOut.data.numpy()[0,0] #extract value 0.0716
        xList.append(x)
        yList.append(y)
        uList.append(u)
        vList.append(v)

xx = np.array(xList)
yy = np.array(yList)
uu = np.array(uList)
vv = np.array(vList)

fig0 = ff.create_quiver(xx, yy, uu, vv,
       scale=.4,
       arrow_scale=.4,
       name='quiver'
       #,line=dict(width=2)
      )

fig0.layout.title = 'Coherence field for time ' + str(t)
fig0.add_trace(f.data[4])
fig0.add_trace(f.data[5])
offline.iplot(fig0)
layout.title
```

    layout.Title({
        'text': 'Initial Curves'
    })

```python
#create a matrix of mesh values
mm,cc = np.meshgrid(np.arange(-0., 1.2, .1), np.arange(-.5, 1, .1))

t = .2
ma = np.array(mm)
ca = np.array(cc)
xList = []
yList = []
uList = []
vList = []
for mi in range(ma.shape[0]):
    for ci in range(ca.shape[1]):
        cIn = torch.Tensor([[t,ma[mi,ci], ca[mi,ci]]])
        cohOut = cohModel(cIn)
        x = ma[mi,ci]
        y = ca[mi,ci]
        u = 0
        v = cohOut.data.numpy()[0,0] #extract value 0.0716
        xList.append(x)
        yList.append(y)
        uList.append(u)
        vList.append(v)

xx = np.array(xList)
yy = np.array(yList)
uu = np.array(uList)
vv = np.array(vList)

fig0 = ff.create_quiver(xx, yy, uu, vv,
       scale=.4,
       arrow_scale=.4,
       name='quiver'
       #,line=dict(width=2)
      )

fig0.layout.title = 'Coherence field for time ' + str(t)
fig0.add_trace(f.data[2])
fig0.add_trace(f.data[3])
offline.iplot(fig0)
layout.title
```

    layout.Title({
        'text': 'Initial Curves'
    })

```python
#create a matrix of mesh values
mm,cc = np.meshgrid(np.arange(-0., 1.1, .1), np.arange(-.8, 1, .1))

t = .0

ma = np.array(mm)
ca = np.array(cc)
xList = []
yList = []
uList = []
vList = []
for mi in range(ma.shape[0]):
    for ci in range(ca.shape[1]):
        cIn = torch.Tensor([[t,ma[mi,ci], ca[mi,ci]]])
        cohOut = cohModel(cIn)
        x = ma[mi,ci]
        y = ca[mi,ci]
        u = 0
        v = cohOut.data.numpy()[0,0] #extract value 0.0716
        xList.append(x)
        yList.append(y)
        uList.append(u)
        vList.append(v)

xx = np.array(xList)
yy = np.array(yList)
uu = np.array(uList)
vv = np.array(vList)

fig0 = ff.create_quiver(xx, yy, uu, vv,
       scale=.3,
       arrow_scale=.2,
       name='quiver'
       #,line=dict(width=2)
      )

fig0.layout.title = 'Coherence field for time ' + str(t)
fig0.add_trace(f.data[0])
fig0.add_trace(f.data[1])
offline.iplot(fig0)
layout.title
```

    layout.Title({
        'text': 'Initial Curves'
    })

```python
##################
tindex=6 #.9
cohMult = [.1,.08, .06,.05,.05,.02,.01] #9.2
cohMult = [.1,.1, .08,.05,.05,.02,.01] #9
cohMult = [.06,.06,.06,.05,.05,.05,.05] #8.5
cohMult = [.09,.08, .06,.05,.05,.02,.02] #8
cohMult = [.15,.1, .08,.06,.05,.04,.01] #8
cohMult = [.1,.1,.08,.06,.05,.05,.04] #7

cohMult = [.1,.07, .06,.05,.04,.02,.01, .01] #7
cohMult = [.09,.07, .06,.05,.04,.02,.01, .01] #9
cohMult = [.1,.08, .06,.05,.05,.02,.01, .01] #9.3

cohMult = [.1,.1, .1,.1,.02,.02,.01, .01] #9.3

#create a random input
relIn = torch.Tensor(gy.createRmInput(morphIndices, timeIndices, sPermutation=[]))
genIn = torch.Tensor(np.random.random([1,9,3]))

relIn[6].shape
```

    torch.Size([9, 2])

```python
#target
morphIndices=[0.,0.1,.2,.3,.5,.7,.9,1.0, 1.1]
timeIndices=[0.,0.1,.2,.3,.5,.7,.9,1.0, 1.1]

#sample source templates to get ph samples
relIn1 = gy.createRmInput(morphIndices, [.9], sPermutation=[])
tmOut0 = tmModel0(torch.Tensor(relIn1))
relOut0 = relModel(tmOut0)

##target C values
relOut0
```

    tensor([[[ 0.9553, -0.0019,  0.2053],
             [ 0.9269,  0.1252,  0.1669],
             [ 0.9303,  0.2330,  0.1373],
             [ 0.9292,  0.3261,  0.1097],
             [ 0.9239,  0.5061,  0.0555],
             [ 0.9058,  0.6967, -0.0018],
             [ 0.8912,  0.9142, -0.0719],
             [ 0.8851,  1.0384, -0.1176],
             [ 0.8720,  1.1683, -0.1716]]], grad_fn=<EluBackward>)

```python
relCout2
```

    tensor([[[0.9000, 0.0000, 0.3252],
             [0.9000, 0.1000, 0.1192],
             [0.9000, 0.2000, 0.3855],
             [0.9000, 0.3000, 0.3413],
             [0.9000, 0.5000, 0.4595],
             [0.9000, 0.7000, 0.2480],
             [0.9000, 0.9000, 0.4136],
             [0.9000, 1.0000, 0.3201],
             [0.9000, 1.1000, 0.2919]]], grad_fn=<CopySlices>)

```python
cohMult[0], cohOut2[:,:,0],relCout2a
```

    (0.1,
     tensor([[-0.0260,  0.0343, -0.0675, -0.0603, -0.1625, -0.0780, -0.2444, -0.1904,
              -0.1951]], grad_fn=<SelectBackward>),
     tensor([[[0.9000, 0.0000, 0.3226],
              [0.9000, 0.1000, 0.1227],
              [0.9000, 0.2000, 0.3788],
              [0.9000, 0.3000, 0.3353],
              [0.9000, 0.5000, 0.4433],
              [0.9000, 0.7000, 0.2402],
              [0.9000, 0.9000, 0.3892],
              [0.9000, 1.0000, 0.3011],
              [0.9000, 1.1000, 0.2724]]], grad_fn=<CopySlices>))

```python
relCout2-relCout2a
```

    tensor([[[ 0.0000,  0.0000,  0.0026],
             [ 0.0000,  0.0000, -0.0034],
             [ 0.0000,  0.0000,  0.0067],
             [ 0.0000,  0.0000,  0.0060],
             [ 0.0000,  0.0000,  0.0163],
             [ 0.0000,  0.0000,  0.0078],
             [ 0.0000,  0.0000,  0.0244],
             [ 0.0000,  0.0000,  0.0190],
             [ 0.0000,  0.0000,  0.0195]]], grad_fn=<SubBackward0>)

```python
cohMult[0],relCout2a
```

    (0.1, tensor([[[0.9000, 0.0000, 0.4954],
              [0.9000, 0.1000, 0.1019],
              [0.9000, 0.2000, 0.4173],
              [0.9000, 0.3000, 0.4035],
              [0.9000, 0.5000, 0.4899],
              [0.9000, 0.7000, 0.4686],
              [0.9000, 0.9000, 0.0827],
              [0.9000, 1.0000, 0.4524],
              [0.9000, 1.1000, 0.2131]]], grad_fn=<CopySlices>))

```python
genOut1 = genModel(genIn) #random ph1

relCout1 = relModel(genOut1) #rel
relCout1[0,:,0:2] = relIn[tindex] #adjust t,m
genOut2 = genModel(relCout1) #ph2

cohOut1 = cohModel(relCout1)
relCout1a = relCout1.clone().detach() #clone
relCout1a[0:1,:,2] += cohOut1[:,:,0]*cohMult[0] #coherence adjusted context
genOut2a = genModel(relCout1a) #ph2a

relCout2 = relModel(genOut2a) #rel
relCout2[0,:,0:2] = relIn[tindex] #adjust t,m
genOut3 = genModel(relCout2) #ph3

cohOut2 = cohModel(relCout2)
relCout2a = relCout2.clone().detach() #clone
relCout2a[0:1,:,2] += cohOut2[:,:,0]*cohMult[1] #coherence adjusted context
genOut3a = genModel(relCout2a) #ph3a

```

```python
ph0 = genOut1.data.numpy()[0]
ph1 = genOut2.data.numpy()[0]
ph2 = genOut2a.data.numpy()[0]
ph3 = genOut3.data.numpy()[0]
ph4 = genOut3a.data.numpy()[0]
# ph5 = genOut6.data.numpy()[0]
# ph6 = genOut7.data.numpy()[0]
# ph7 = genOut8.data.numpy()[0]
# ph8 = genOut9.data.numpy()[0]
# ph9 = genOut10.data.numpy()[0]
# ph10 = genOut11.data.numpy()[0]
# ph11 = genOut12.data.numpy()[0]
# ph12 = genOut13.data.numpy()[0]

# nx = np.concatenate((ph1,ph1))
# nx = np.concatenate((ph0,ph12))
nx = np.concatenate((ph0,ph4))

gx = gy.FormatNetworkToGraphColumns(nx, xOffset=0, yOffset=14, colPoints=7)

layout = go.Layout(
    title = 'Initial Curves',
    height = 600,
    width = 600,
    hovermode= 'closest',
    xaxis= dict(
        title= 'X',
        range = [-.8,1.2],
    ),
    yaxis=dict(
        title= 'Z',
        range = [-.2,.8],
    )
)
f = gy.graphCurves(gx, layout, dashRange=range(0,9))
offline.iplot(f)

```

```python
gx = gy.FormatNetworkToGraphColumns(nx, xOffset=7, yOffset=14, colPoints=7)

layout = go.Layout(
    title = 'Initial Curves',
    height = 600,
    width = 600,
    hovermode= 'closest',
    xaxis= dict(
        title= 'Y',
        range = [-1.,1.2],
    ),
    yaxis=dict(
        title= 'Z',
        range = [-.2,.8],
    )
)
f = gy.graphCurves(gx, layout, dashRange=range(0,9))
offline.iplot(f)
```

```python
cohMult = [.06,.06,.06,.05,.05,.05,.05] #8.5

gx = gy.FormatNetworkToGraphColumns(nx, xOffset=0, yOffset=7, colPoints=7)

layout = go.Layout(
    title = 'Initial Curves',
    height = 600,
    width = 600,
    hovermode= 'closest',
    xaxis= dict(
        title= 'X',
        range = [-.6,1],
    ),
    yaxis=dict(
        title= 'Y',
        range = [-1.3,1.3],
    )
)
f = gy.graphCurves(gx, layout, dashRange=range(0,9))
offline.iplot(f)
```

```python
cohMult = [.15,.1, .08,.06,.05,.04,.01] #8

gx = gy.FormatNetworkToGraphColumns(nx, xOffset=0, yOffset=7, colPoints=7)

layout = go.Layout(
    title = 'Initial Curves',
    height = 600,
    width = 600,
    hovermode= 'closest',
    xaxis= dict(
        title= 'X',
        range = [-.6,1],
    ),
    yaxis=dict(
        title= 'Y',
        range = [-1.3,1.3],
    )
)
f = gy.graphCurves(gx, layout, dashRange=range(0,9))
offline.iplot(f)
```

```python
'''
9. Generate blender data from template interpolation model output

#phOut.shape torch.Size([10, 7, 23])

'''

# phOut = phModel(torch.Tensor(torch.Tensor(inpt0)))

# x = phOut.detach().numpy()
# fb = gy.FormatNetworkToBlender(x, xOffset=0, yOffset=7, zOffset=14, colPoints=7)

# with open(CURRENT_MODEL_SAVE_DIR +'btest.json', 'w') as fp:
#     json.dump(fb.tolist(),fp)

# x.shape, fb.shape
```


# Long range interaction

Computation of long range phase shifts on the lattice.

This notebook shall identify a local potential with long range components (non-zero effective range but sparse in coordinate or momentum space).
Preferably, the result should be cutoff independent.
For now, $S$-wave results are sufficinet.
The current idea is to choose a potential which mimics chiral LO interactions (contact + one pion exchange) and numerically compute the phase shifts with high precision.

$$\renewcommand{\vec}[1]{\boldsymbol{#1}}$$

---
**Notes:** I first analyzed a separable potential $V(p, p') = - g^*(p) g(p')$ but this is very expensive to compute on the lattice (dense Hamiltonian).

## Numerical compututation of phase shifts

This section provides a numerical extraction of phase shifts.

### Init

Modules:

```python
import numpy as np
from itertools import product
import matplotlib.pylab as plt

from opdens.utilities.mesh import trns
```

Parameters (in MeV)

```python
HBARC = 197
mN = 939 /HBARC
mu = mN / 2
```

Momentum grid points:

```python
p, wp = trns(40, 20)

p = np.linspace(1.e-6, 20, 200)
wp = np.ones(p.size) * (p[1] - p[0])

H0 = np.diag(p**2 / 2 / mu)
```

```python
1 / pp0**2
```

```python
plt.plot(p, 2 * mu * wp / (pp0**2 - p**2), "o")
```

```python
#p, wp = trns(100, 100)
pp0 =  15

gint = np.sum(2 * mu * wp / (pp0**2 - p**2 ))

print(gint, np.log((p[-1] + pp0) / (p[-1] - pp0)) * mu / pp0 )
print(np.log((p[-1] + pp0) / (p[-1] - pp0)) * mu / pp0 - gint)
```

### Idea

Compute the $T$-matrix numerically on a partial wave grid $\left| k, \alpha\right\rangle$

\begin{align}
    T_{\alpha'\alpha}\left(k^{\prime}, k ; E\right)
    &=
    V_{\alpha'\alpha}\left(k^{\prime}, k\right)
    +
    \sum_{\alpha''}
    \int d q q^{2}
    \frac{V_{\alpha'\alpha''}\left(k^{\prime}, q\right) T_{\alpha''\alpha}(q, k ; E)}{E-E_{q}+i \epsilon}
    \\
    &=
    V_{\alpha'\alpha}\left(k^{\prime}, k\right)
    +
    \sum_{\alpha''}\mathcal P \int_{0}^{\infty} d q q^{2}
        \frac{V_{\alpha'\alpha''}\left(k^{\prime}, q\right) T_{\alpha''\alpha}(q, k ; E)}{E-E_{q}}
    -  i \pi \sum_{\alpha''}\int_{0}^{\infty} d q q^{2} V_{\alpha'\alpha''}\left(k^{\prime}, q\right) T_{\alpha''\alpha}(q, k ; E) \delta(E-E_{q})
    \\
    &=
    V_{\alpha'\alpha}\left(k^{\prime}, k\right)
    +
    \sum_{\alpha''}\mathcal P \int_{0}^{\infty} d q q^{2}
        \frac{V_{\alpha'\alpha''}\left(k^{\prime}, q\right) T_{\alpha''\alpha}(q, k ; E)}{E-E_{q}}
    -  i \pi \sum_{\alpha''} \left[ \mu q \, V_{\alpha'\alpha''}\left(k^{\prime}, q\right) T_{\alpha''\alpha}(q, k ; E) \right]_{q = \sqrt{2 \mu E}}
\end{align}

where $E_q = q^2 / (2 \mu)$.

### Delta identity

In the above equation it was used that
$$
    \int d x \frac{1}{f(x) + i \epsilon} = \mathcal P \int d x \frac{1}{f(x)} - i \pi  \int d x \delta(f(x))
$$
where $\mathcal P$ denotes the principle value integral and
$$
    \int d x \delta(f(x)) g(x) = \sum\limits_{x_i} \frac{g(x_i) }{|f'(x_i)|}
$$
where $x_i$ are the zeros of $f$.

### Basis definitions

The partial wave decomposed matrix element of operators are defined as
$$
    O_{\alpha' \alpha}(k', k)
    =
    \int d \Omega_{k'}  d \Omega_{k}
    Y_{l' m_l'} (\Omega_{k'})
    \left\langle \vec k' , \tilde \alpha' \middle\vert O \middle\vert \vec k, \tilde \alpha \right\rangle
    Y_{l m_l} (\Omega_{k})
$$
with the collection of quantum numbers $\alpha = (\tilde \alpha, l , m_l)$.

More specifically, the angular matrix elements are defined as

\begin{align}
    \left\langle \vec k \middle\vert q l m_l \right\rangle
    &=
    \frac{\delta(k - q)}{q^2} Y_{l m_l}^*(\Omega_k)
    \, , &
    \mathbb{1}
    &=
    \sum\limits_{l m} \int d q q^2 \left\vert q l m \right\rangle\left\langle q l m \right \vert
\end{align}

In case of $S$-wave only results, this becomes
$$
    O_{0}(k', k) =
    \frac{1}{4 \pi}
    \int d \Omega_{k'}  d \Omega_{k}
    \left\langle \vec k' \middle\vert O \middle\vert \vec k \right\rangle
$$

### Simplifications

$$
    \frac{1}{X+i \epsilon}=\mathcal P \frac{1}{X}-i \pi \delta(X)
    \qquad
    \int d x \,  f(x) \delta(g(x)) = \sum_{x_i} \frac{f(x_i)}{|g'(x_i)|}
$$
was used and $g(x_i) = 0$ are all zeros in the integration domain.
The result is

$$
    \int_{0}^{\infty} d q q^{2} V_{\alpha'\alpha''}\left(k^{\prime}, q\right) T_{\alpha''\alpha}(q, k ; E) \delta(E-E_{q})
    =
    \frac{\mu}{p_0} p_0^2 \, V_{\alpha'\alpha''}\left(k^{\prime}, p_0\right) T_{\alpha''\alpha}(p_0, k ; E) \bigg|_{p_0 = \sqrt{2 \mu E}}
$$

### Smoothing out

The above equation becomes singular for $E_q = E$.
Eventhough this value is ruled out by the principle value integral, it can be numerically unstable.
For this reason it is replaced by a more stable equivalent.

\begin{multline}
    P \int_{0}^{\infty} d q q^{2}
    \frac{V_{\alpha'\alpha''}\left(k^{\prime}, q\right) T_{\alpha''\alpha}(q, k ; E)}{E-E_{q}}
    \\=
    P \int_{0}^{\infty} d q
    \frac{q^{2} V_{\alpha'\alpha''}\left(k^{\prime}, q\right)T_{\alpha''\alpha}(q, k ; E) - p_0^2 V_{\alpha'\alpha''}\left(k^{\prime}, p_0 \right)T_{\alpha''\alpha}(p_0, k ; E)  }{E-E_{q}}
    \\+
    P \int_{0}^{\infty} d q
    \frac{1}{E-E_{q}} p_0^2 V_{\alpha'\alpha''}\left(k^{\prime}, p_0 \right) T_{\alpha''\alpha}(p_0, k ; E)
\end{multline}
where $p_0 = \sqrt{2 \mu E}$.

The last part is equal to
$$
    P \int_{0}^{\Lambda} d q
    \frac{1}{E-E_{q}}
    =
    \frac{\mu}{p_0} \log \left(\frac{\Lambda +p_0}{\Lambda -p_0}\right)
    \, , \qquad \Lambda \geq p_0
$$
Note that this integral is zero for infinite cutoff.

Thus, the regularized integral becomes

\begin{multline}
    T_{\alpha'\alpha}\left(k^{\prime}, k ; E\right)
    =
    V_{\alpha'\alpha}\left(k^{\prime}, k\right)
    + P \int_{0}^{\Lambda} d q
    \frac{q^{2} V_{\alpha'\alpha''}\left(k^{\prime}, q\right)T_{\alpha''\alpha}(q, k ; E) - p_0^2 V_{\alpha'\alpha''}\left(k^{\prime}, p_0 \right)T_{\alpha''\alpha}(p_0, k ; E)  }{E-E_{q}}
    \\
    +  \mu p_0 \left[
        \log \left(\frac{\Lambda +p_0}{\Lambda -p_0}\right) -  i \pi
    \right]
    \sum_{\alpha''}  V_{\alpha'\alpha''}\left(k^{\prime}, p_0\right) T_{\alpha''\alpha}(p_0, k ; E)
\end{multline}

### Discretization

The on-shell equation of the $T$-matrix for $S$-waves only on a numerical grid ($(k_i, w_{k_i})$, $1 \leq i \leq N$) becomes a matrix-vector equation:
\begin{align}
    t_0 &= T_0\left(p_0, p_0, E = \frac{p_0^2}{2 \mu}\right)
    \, , &
    t_i &= T_0\left(k_i, p_0, E = \frac{p_0^2}{2 \mu}\right)
    \, , &
    v_i &= V_0(k_i, p_0)
    \, , &
    v_{i, j} &= V_0(k_i,k_j)
    \, , &
    g_i &= \frac{2 \mu}{p_0^2 - k_i^2}
\end{align}

$$
    t_i
    =
    v_i
    + \sum\limits_{n=1}^N w_{k_n} \left(k_n^2  v_{i n} t_n - p_0^2 v_{i} t_0\right) g_n
    + \mu p_0 \left[\log\left(\frac{\Lambda + p_0}{\Lambda - p_0} \right) -  i \pi  \right] v_i t_0
    \, , \qquad  1 \leq i \leq N \, .
$$

One can naturally extend this equation by choosing $k_0 = p_0$ and $k_0 \neq k_i \forall i \neq 0$ such that one finds

\begin{align}
    \sum_{n=0}^N A_{in} t_n &= v_i
    \, , &
    A_{in} &=
    \delta_{in} +
    \begin{cases}
         - w_{k_n} k_n^2  v_{i n} g_n & n > 0  \\
       \mu p_0 \left[
           i \pi  - \log\left(\frac{\Lambda + p_0}{\Lambda - p_0}\right)
           + \frac{p_0}{\mu} \sum\limits_{n=1}^N w_{k_n} g_n
       \right] v_i
       & n = 0
    \end{cases}
\end{align}

## Potentials

Choose the Yukawa potential
$$
    V(\vec p, \vec k)
    =
    - \frac{g_A^2}{2 F_\pi^2}
    \frac{m_\pi^2}{(\vec p - \vec k)^2 + m_\pi^2}
    \leftrightarrow
    V(r)
    =
   -\frac{m_{\pi }^2 g_A^2}{8 \pi  F_{\pi }^2} \frac{e^{-m_{\pi }r}}{ r}
$$

$g_A = 1.29$, $f_\pi = 132$MeV and $m_\pi = 141$MeV
The $l = 0$ component is given by

$$
\left\langle\mathbf{k}^{\prime}|\widehat{V}| \mathbf{k}\right\rangle=\frac{2}{\pi} \sum_{l, m} V_{l}\left(k^{\prime}, k\right) Y_{l m}^{*}\left(\Omega_{k^{\prime}}\right) Y_{l m}\left(\Omega_{k}\right)
\qquad
V_{l}\left(k^{\prime}, k\right)=\int_{0}^{\infty} r^{2} d r \, j_{l}(k r) V(r) j_{l}\left(k^{\prime} r\right)
$$
adn thus
$$
 V_0(k, k') = \frac{m_{\pi }^2 g_A^2 }{32 \pi
   F_{\pi }^2 } \frac{1}{k k'}\log \left(1-\frac{4 k k'}{(k+k')^2+m_{\pi }^2}\right)
$$

The partial wave decomposed matrix element of operators are defined as
$$
    O_{\alpha' \alpha}(k', k)
    =
    \int d \Omega_{k'}  d \Omega_{k}
    Y_{l' m_l'} (\Omega_{k'})
    \left\langle \vec k' , \tilde \alpha' \middle\vert O \middle\vert \vec k, \tilde \alpha \right\rangle
    Y_{l m_l} (\Omega_{k})
$$
with the collection of quantum numbers $\alpha = (\tilde \alpha, l , m_l)$.

More specifically, the angular matrix elements are defined as

\begin{align}
    \left\langle \vec k \middle\vert q l m_l \right\rangle
    &=
    \frac{\delta(k - q)}{q^2} Y_{l m_l}^*(\Omega_k)
    \, , &
    \mathbb{1}
    &=
    \sum\limits_{l m} \int d q q^2 \left\vert q l m \right\rangle\left\langle q l m \right \vert
\end{align}

In case of $S$-wave only results, this becomes
$$
    O_{0}(k', k) =
    \frac{1}{4 \pi}
    \int d \Omega_{k'}  d \Omega_{k}
    \left\langle \vec k' \middle\vert O \middle\vert \vec k \right\rangle
$$

```python
def V1(k, mpi=141., gA=1.29, fpi=132.):
    kk = k.reshape(-1, 1)
    return gA**2 * mpi**2/ 32 / np.pi / fpi**2 / kk / k * np.log(1 - 4 * kk * k / ((k+kk)**2 + mpi**2))

def V(k):
    return np.ones([k.size]*2)
```

## Computing T

```python
p0 = [(pp + p[n+1])/2 for n, pp in enumerate(p[:-1])][:10]

phase_shifts = []

for pp0 in p0:
    pp = np.append([pp0], p)
    v = -V(pp)
    E = pp**2 / 2 / mu

    A = np.zeros([pp.size]*2, dtype=complex)

    gint = np.sum(wp / (E[0] - E[1:]))

    print(- np.log((pp[-1] + pp0) / (pp[-1] - pp0)) * mu / pp0 + gint)

    for i, n in product(*[range(pp.size), range(pp.size)]):

        if i == n:
            A[i, i] = 1

        if n == 0:
            A[i, n] += mu * pp0 * (
                1j * np.pi
                - np.log((pp[-1] + pp0) / (pp[-1] - pp0))
                + gint * pp0 / mu
            )* v[i, 0]
        else:
            A[i, n] -= wp[n-1] * p[n-1]**2 * v[i, n] / (E[0] - E[n])

    t = np.linalg.solve(A , np.diagonal(v))

    f = mu * t / 2 / np.pi

    ere = 1 / f + 1j * pp0

    phase_shifts.append(ere[0])

phase_shifts = np.array(phase_shifts)
```

### Result

$$
    C_{0}(\Lambda)=\frac{2 \pi / \mu}{1 / a_{0}-2 \Lambda / \pi}
$$

```python
1/t.real
```

```python
c0 = - V(np.array([1])).mean()
Lambda = p[-1]
a0 =  1 / (2 * np.pi / mu / c0 + 2 * Lambda / np.pi)

ere.real.mean() * a0
```

```python
fig, ax = plt.subplots()

ax.plot(p0, phase_shifts.real, ".", label="Re")
ax.plot(p0, phase_shifts.imag, "--", label="Im")

#ax.axhline(-a0inv, lw=5)

```

```python
fig, ax = plt.subplots()

ax.plot(pp[1:], A[0, 1:].imag, ".")
ax.axvline(pp[0])

ax.set_ylim(-10, 10)

plt.show()
```

```python
plt.plot(p0, phase_shifts.imag, ".")
```

```python
(1 / t).real
```

Using
\begin{align}
    T_{l}\left(k^{\prime}, k ; E\right)
    &=
    V_{l}\left(k^{\prime}, k\right)+\frac{2}{\pi}
    \int_{0}^{\infty} d q q^{2}
    \frac{V_{l}\left(k^{\prime}, q\right) T_{l}(q, k ; E)}{E-E_{q}+i \epsilon}
    \\
    &=
    V_{l}\left(k^{\prime}, k\right)
    +\frac{2}{\pi} \mathcal P \int_{0}^{\infty} d q q^{2}
    \frac{V_{l}\left(k^{\prime}, q\right) T_{l}(q, k ; E)}{E-E_{q}}
    - 2 i \int_{0}^{\infty} d q q^{2} V_{l}\left(k^{\prime}, q\right) T_{l}(q, k ; E) \delta(E-E_{q})
\end{align}
where
$$
    \frac{1}{X+i \epsilon}=\mathcal P \frac{1}{X}-i \pi \delta(X)
    \qquad
    \int d x \,  f(x) \delta(g(x)) = \sum_{x_i} \frac{f(x_i)}{|g'(x_i)|}
$$
was used and $g(x_i) = 0$ are all zeros in the integration domain.
The result is

$$
    \int_{0}^{\infty} d q q^{2} V_{l}\left(k^{\prime}, q\right) T_{l}(q, k ; E) \delta(E-E_{q})
    =
    \frac{\mu}{q} q^2 \, V_{l}\left(k^{\prime}, q\right) T_{l}(q, k ; E) \bigg|_{q = \sqrt{2 \mu E}}
$$

We can write the on shell $T$ matrix equation as
$$
    \vec t = \vec v + M \vec t \Rightarrow (1 - M) \vec t = \vec v
$$
with
\begin{align}
    t_i &= T_0(k_i, k_i, E_{k_i})
    \, , &
    v_i &= V_0(k_i, k_i, E_{k_i})
    \, , &
    M_{ij} &=
    \begin{cases}
        \frac{2}{\pi} k_j^2 w_{k_j} \frac{V_0(k_i, k_j)}{E_{k_i} - E_{k_j}} & i \neq j \\
        - 2 i \, \mu k_i \, V_{0}\left(k_i, k_i\right) & i = j
    \end{cases}
\end{align}


# BSSN Time-Evolution Equations for the Gauge Fields $\alpha$ and $\beta^i$

## Author: Zach Etienne
### Formatting improvements courtesy Brandon Clark

[comment]: <> (Abstract: TODO, or make the introduction an abstract and addiotnal notes section, and write a new Introduction)

**Notebook Status:**  Validated

**Validation Notes:** All expressions generated in this module have been validated against a trusted code (the original NRPy+/SENR code, which itself was validated against [Baumgarte's code](https://arxiv.org/abs/1211.6632)).

### NRPy+ Source Code for this module: [BSSN/BSSN_gauge_RHSs.py](../edit/BSSN/BSSN_gauge_RHSs.py)

## Introduction:
This tutorial notebook constructs SymPy expressions for the right-hand sides of the time-evolution equations for the gauge fields $\alpha$ (the lapse, governing how much proper time elapses at each point between one timestep in a 3+1 solution to Einstein's equations and the next) and $\beta^i$ (the shift, governing how much proper distance numerical grid points move from one timestep in a 3+1 solution to Einstein's equations and the next).

Though we are completely free to choose gauge conditions (i.e., free to choose the form of the right-hand sides of the gauge time evolution equations), very few have been found robust in the presence of (puncture) black holes. So we focus here only on a few of the most stable choices.

# Table of Contents
$$\label{toc}$$

This notebook is organized as follows

1. [Step 1](#initializenrpy): Initialize needed Python/NRPy+ modules
1. [Step 2](#lapseconditions): Right-hand side of $\partial_t \alpha$
    1. [Step 2.a](#onepluslog): $1+\log$ lapse
    1. [Step 2.b](#harmonicslicing): Harmonic slicing
    1. [Step 2.c](#frozen): Frozen lapse
1. [Step 3](#shiftconditions): Right-hand side of $\partial_t \beta^i$: Second-order Gamma-driving shift conditions
    1. [Step 3.a](#origgammadriving): Original, non-covariant Gamma-driving shift condition
    1. [Step 3.b](#covgammadriving): [Brown](https://arxiv.org/abs/0902.3652)'s suggested covariant Gamma-driving shift condition
        1. [Step 3.b.i](#partial_beta): The right-hand side of the $\partial_t \beta^i$ equation
        1. [Step 3.b.ii](#partial_upper_b): The right-hand side of the $\partial_t B^i$ equation
1. [Step 4](#rescale): Rescale right-hand sides of BSSN gauge equations
1. [Step 5](#code_validation): Code Validation against `BSSN.BSSN_gauge_RHSs` NRPy+ module
1. [Step 6](#latex_pdf_output): Output this notebook to $\LaTeX$-formatted PDF file

# Step 1: Initialize needed Python/NRPy+ modules \[Back to [top](#toc)\]
$$\label{initializenrpy}$$

Let's start by importing all the needed modules from Python/NRPy+:

```python
# Step 1: Import all needed modules from NRPy+:
import sympy as sp                # SymPy: The Python computer algebra package upon which NRPy+ depends
import NRPy_param_funcs as par    # NRPy+: Parameter interface
import grid as gri                # NRPy+: Functions having to do with numerical grids
import indexedexp as ixp          # NRPy+: Symbolic indexed expression (e.g., tensors, vectors, etc.) support
import reference_metric as rfm    # NRPy+: Reference metric support
import BSSN.BSSN_quantities as Bq # NRPy+: Computes useful BSSN quantities
import BSSN.BSSN_RHSs as Brhs     # NRPy+: Constructs BSSN right-hand-side expressions
import sys                        # Standard Python modules for multiplatform OS-level functions

# Step 1.c: Declare/initialize parameters for this module
thismodule = "BSSN_gauge_RHSs"
par.initialize_param(par.glb_param("char", thismodule, "LapseEvolutionOption", "OnePlusLog"))
par.initialize_param(par.glb_param("char", thismodule, "ShiftEvolutionOption", "GammaDriving2ndOrder_Covariant"))

# Step 1.d: Set spatial dimension (must be 3 for BSSN, as BSSN is
#           a 3+1-dimensional decomposition of the general
#           relativistic field equations)
DIM = 3

# Step 1.e: Given the chosen coordinate system, set up
#           corresponding reference metric and needed
#           reference metric quantities
# The following function call sets up the reference metric
#    and related quantities, including rescaling matrices ReDD,
#    ReU, and hatted quantities.
rfm.reference_metric()

# Step 1.f: Define BSSN scalars & tensors (in terms of rescaled BSSN quantities)
import BSSN.BSSN_quantities as Bq
Bq.BSSN_basic_tensors()
Bq.betaU_derivs()

import BSSN.BSSN_RHSs as Brhs
Brhs.BSSN_RHSs()
```

# Step 2: Right-hand side of $\partial_t \alpha$ \[Back to [top](#toc)\]
$$\label{lapseconditions}$$

## Step 2.a: $1+\log$ lapse \[Back to [top](#toc)\]
$$\label{onepluslog}$$

The [$1=\log$ lapse condition](https://arxiv.org/abs/gr-qc/0206072) is a member of the [Bona-Masso family of lapse choices](https://arxiv.org/abs/gr-qc/9412071), which has the desirable property of singularity avoidance. As is common (e.g., see [Campanelli *et al* (2005)](https://arxiv.org/abs/gr-qc/0511048)), we make the replacement $\partial_t \to \partial_0 = \partial_t + \beta^i \partial_i$ to ensure lapse characteristics advect with the shift. The bracketed term in the $1+\log$ lapse condition below encodes the shift advection term:

\begin{align}
\partial_0 \alpha &= -2 \alpha K \\
\implies \partial_t \alpha &= \left[\beta^i \partial_i \alpha\right] - 2 \alpha K
\end{align}

```python
# Step 2.a: The 1+log lapse condition:
#   \partial_t \alpha = \beta^i \alpha_{,i} - 2*\alpha*K
# First import expressions from BSSN_quantities
cf    = Bq.cf
trK   = Bq.trK
alpha = Bq.alpha
betaU = Bq.betaU

# Implement the 1+log lapse condition
if par.parval_from_str(thismodule+"::LapseEvolutionOption") == "OnePlusLog":
    alpha_rhs = -2*alpha*trK
    alpha_dupD = ixp.declarerank1("alpha_dupD")
    for i in range(DIM):
        alpha_rhs += betaU[i]*alpha_dupD[i]
```

## Step 2.b: Harmonic slicing \[Back to [top](#toc)\]
$$\label{harmonicslicing}$$

As defined on Pg 2 of https://arxiv.org/pdf/gr-qc/9902024.pdf , this is given by

$$
\partial_t \alpha = \partial_t e^{6 \phi} = 6 e^{6 \phi} \partial_t \phi
$$

If

$$\text{cf} = W = e^{-2 \phi},$$

then

$$
6 e^{6 \phi} \partial_t \phi = 6 W^{-3} \partial_t \phi.
$$

However,
$$
\partial_t \phi = -\partial_t \text{cf} / (2 \text{cf})$$

(as described above), so if `cf`$=W$, then
\begin{align}
\partial_t \alpha &= 6 e^{6 \phi} \partial_t \phi \\
&= 6 W^{-3} \left(-\frac{\partial_t W}{2 W}\right) \\
&= -3 \text{cf}^{-4} \text{cf}\_\text{rhs}
\end{align}

**Exercise to students: Implement Harmonic slicing for `cf`$=\chi$**

```python
# Step 2.b: Implement the harmonic slicing lapse condition
if par.parval_from_str(thismodule+"::LapseEvolutionOption") == "HarmonicSlicing":
    if par.parval_from_str("BSSN.BSSN_quantities::EvolvedConformalFactor_cf") == "W":
        alpha_rhs = -3*cf**(-4)*Brhs.cf_rhs
    elif par.parval_from_str("BSSN.BSSN_quantities::EvolvedConformalFactor_cf") == "phi":
        alpha_rhs = 6*sp.exp(6*cf)*Brhs.cf_rhs
    else:
        print("Error LapseEvolutionOption==HarmonicSlicing unsupported for EvolvedConformalFactor_cf!=(W or phi)")
        sys.exit(1)
```

## Step 2.c: Frozen lapse \[Back to [top](#toc)\]
$$\label{frozen}$$

This slicing condition is given by
$$\partial_t \alpha = 0,$$

which is rarely a stable lapse condition.

```python
# Step 2.c: Frozen lapse
#    \partial_t \alpha = 0
if par.parval_from_str(thismodule+"::LapseEvolutionOption") == "Frozen":
    alpha_rhs = sp.sympify(0)
```

# Step 3: Right-hand side of $\partial_t \beta^i$: Second-order Gamma-driving shift conditions \[Back to [top](#toc)\]
$$\label{shiftconditions}$$

The motivation behind Gamma-driving shift conditions are well documented in the book [*Numerical Relativity* by Baumgarte & Shapiro](https://www.amazon.com/Numerical-Relativity-Einsteins-Equations-Computer/dp/052151407X/).

## Step 3.a: Original, non-covariant Gamma-driving shift condition \[Back to [top](#toc)\]
$$\label{origgammadriving}$$

**Option 1: Non-Covariant, Second-Order Shift**

We adopt the [*shifting (i.e., advecting) shift*](https://arxiv.org/abs/gr-qc/0605030) non-covariant, second-order shift condition:
\begin{align}
\partial_0 \beta^i &= B^{i} \\
\partial_0 B^i &= \frac{3}{4} \partial_{0} \bar{\Lambda}^{i} - \eta B^{i} \\
\implies \partial_t \beta^i &= \left[\beta^j \partial_j \beta^i\right] + B^{i} \\
\partial_t B^i &= \left[\beta^j \partial_j B^i\right] + \frac{3}{4} \partial_{0} \bar{\Lambda}^{i} - \eta B^{i},
\end{align}
where $\eta$ is the shift damping parameter, and $\partial_{0} \bar{\Lambda}^{i}$ in the right-hand side of the $\partial_{0} B^{i}$ equation is computed by adding $\beta^j \partial_j \bar{\Lambda}^i$ to the right-hand side expression given for $\partial_t \bar{\Lambda}^i$ in the BSSN time-evolution equations as listed [here](Tutorial-BSSN_formulation.ipynb), so no explicit time dependence occurs in the right-hand sides of the BSSN evolution equations and the Method of Lines can be applied directly.

```python
# Step 3.a: Set \partial_t \beta^i
# First import expressions from BSSN_quantities
BU         = Bq.BU
betU       = Bq.betU
betaU_dupD = Bq.betaU_dupD
# Define needed quantities
beta_rhsU = ixp.zerorank1()
B_rhsU = ixp.zerorank1()
if par.parval_from_str(thismodule+"::ShiftEvolutionOption") == "GammaDriving2ndOrder_NoCovariant":
    # Step 3.a.i: Compute right-hand side of beta^i
    # *  \partial_t \beta^i = \beta^j \beta^i_{,j} + B^i
    for i in range(DIM):
        beta_rhsU[i] += BU[i]
        for j in range(DIM):
            beta_rhsU[i] += betaU[j]*betaU_dupD[i][j]
    # Compute right-hand side of B^i:
    eta = par.Cparameters("REAL", thismodule, ["eta"],2.0)

    # Step 3.a.ii: Compute right-hand side of B^i
    # *  \partial_t B^i     = \beta^j B^i_{,j} + 3/4 * \partial_0 \Lambda^i - eta B^i
    # Step 3.a.iii: Define BU_dupD, in terms of derivative of rescaled variable \bet^i
    BU_dupD = ixp.zerorank2()
    betU_dupD = ixp.declarerank2("betU_dupD","nosym")
    for i in range(DIM):
        for j in range(DIM):
            BU_dupD[i][j] = betU_dupD[i][j]*rfm.ReU[i] + betU[i]*rfm.ReUdD[i][j]

    # Step 3.a.iv: Compute \partial_0 \bar{\Lambda}^i = (\partial_t - \beta^i \partial_i) \bar{\Lambda}^j
    Lambdabar_partial0 = ixp.zerorank1()
    for i in range(DIM):
        Lambdabar_partial0[i] = Brhs.Lambdabar_rhsU[i]
    for i in range(DIM):
        for j in range(DIM):
            Lambdabar_partial0[j] += -betaU[i]*Brhs.LambdabarU_dupD[j][i]

    # Step 3.a.v: Evaluate RHS of B^i:
    for i in range(DIM):
        B_rhsU[i] += sp.Rational(3,4)*Lambdabar_partial0[i] - eta*BU[i]
        for j in range(DIM):
            B_rhsU[i] += betaU[j]*BU_dupD[i][j]
```

## Step 3.b: [Brown](https://arxiv.org/abs/0902.3652)'s suggested covariant Gamma-driving shift condition \[Back to [top](#toc)\]
$$\label{covgammadriving}$$

### Step 3.b.i: The right-hand side of the $\partial_t \beta^i$ equation \[Back to [top](#toc)\]
$$\label{partial_beta}$$

This is [Brown's](https://arxiv.org/abs/0902.3652) suggested formulation (Eq. 20b; note that Eq. 20a is the same as our lapse condition, as $\bar{D}_j \alpha = \partial_j \alpha$ for scalar $\alpha$):
$$\partial_t \beta^i = \left[\beta^j \bar{D}_j \beta^i\right] + B^{i}$$
Based on the definition of covariant derivative, we have
$$
\bar{D}_{j} \beta^{i} = \beta^i_{,j} + \bar{\Gamma}^i_{mj} \beta^m,
$$
so the above equation becomes
\begin{align}
\partial_t \beta^i &= \left[\beta^j \left(\beta^i_{,j} + \bar{\Gamma}^i_{mj} \beta^m\right)\right] + B^{i}\\
&= {\underbrace {\textstyle \beta^j \beta^i_{,j}}_{\text{Term 1}}} +
{\underbrace {\textstyle \beta^j \bar{\Gamma}^i_{mj} \beta^m}_{\text{Term 2}}} +
{\underbrace {\textstyle B^i}_{\text{Term 3}}}
\end{align}

```python
# Step 3.b: The right-hand side of the \partial_t \beta^i equation
if par.parval_from_str(thismodule+"::ShiftEvolutionOption") == "GammaDriving2ndOrder_Covariant":
    # Step 3.b Option 2: \partial_t \beta^i = \left[\beta^j \bar{D}_j \beta^i\right] + B^{i}
    # First we need GammabarUDD, defined in Bq.gammabar__inverse_and_derivs()
    Bq.gammabar__inverse_and_derivs()
    GammabarUDD = Bq.GammabarUDD
    # Then compute right-hand side:
    # Term 1: \beta^j \beta^i_{,j}
    for i in range(DIM):
        for j in range(DIM):
            beta_rhsU[i] += betaU[j]*betaU_dupD[i][j]

    # Term 2: \beta^j \bar{\Gamma}^i_{mj} \beta^m
    for i in range(DIM):
        for j in range(DIM):
            for m in range(DIM):
                beta_rhsU[i] += betaU[j]*GammabarUDD[i][m][j]*betaU[m]
    # Term 3: B^i
    for i in range(DIM):
        beta_rhsU[i] += BU[i]
```

### Step 3.b.ii: The right-hand side of the $\partial_t B^i$ equation \[Back to [top](#toc)\]
$$\label{partial_upper_b}$$

$$\partial_t B^i = \left[\beta^j \bar{D}_j B^i\right] + \frac{3}{4}\left( \partial_t \bar{\Lambda}^{i} - \beta^j \bar{D}_j \bar{\Lambda}^{i} \right) - \eta B^{i}$$

Based on the definition of covariant derivative, we have for vector $V^i$
$$
\bar{D}_{j} V^{i} = V^i_{,j} + \bar{\Gamma}^i_{mj} V^m,
$$
so the above equation becomes
\begin{align}
\partial_t B^i &= \left[\beta^j \left(B^i_{,j} + \bar{\Gamma}^i_{mj} B^m\right)\right] + \frac{3}{4}\left[ \partial_t \bar{\Lambda}^{i} - \beta^j \left(\bar{\Lambda}^i_{,j} + \bar{\Gamma}^i_{mj} \bar{\Lambda}^m\right) \right] - \eta B^{i} \\
&= {\underbrace {\textstyle \beta^j B^i_{,j}}_{\text{Term 1}}} +
{\underbrace {\textstyle \beta^j \bar{\Gamma}^i_{mj} B^m}_{\text{Term 2}}} +
{\underbrace {\textstyle \frac{3}{4}\partial_t \bar{\Lambda}^{i}}_{\text{Term 3}}} -
{\underbrace {\textstyle \frac{3}{4}\beta^j \bar{\Lambda}^i_{,j}}_{\text{Term 4}}} -
{\underbrace {\textstyle \frac{3}{4}\beta^j \bar{\Gamma}^i_{mj} \bar{\Lambda}^m}_{\text{Term 5}}} -
{\underbrace {\textstyle \eta B^i}_{\text{Term 6}}}
\end{align}

```python
if par.parval_from_str(thismodule+"::ShiftEvolutionOption") == "GammaDriving2ndOrder_Covariant":
    # Step 3.c: Covariant option:
    #  \partial_t B^i = \beta^j \bar{D}_j B^i
    #               + \frac{3}{4} ( \partial_t \bar{\Lambda}^{i} - \beta^j \bar{D}_j \bar{\Lambda}^{i} )
    #               - \eta B^{i}
    #                 = \beta^j B^i_{,j} + \beta^j \bar{\Gamma}^i_{mj} B^m
    #               + \frac{3}{4}[ \partial_t \bar{\Lambda}^{i}
    #                            - \beta^j (\bar{\Lambda}^i_{,j} + \bar{\Gamma}^i_{mj} \bar{\Lambda}^m)]
    #               - \eta B^{i}
    # Term 1, part a: First compute B^i_{,j} using upwinded derivative
    BU_dupD = ixp.zerorank2()
    betU_dupD = ixp.declarerank2("betU_dupD","nosym")
    for i in range(DIM):
        for j in range(DIM):
            BU_dupD[i][j] = betU_dupD[i][j]*rfm.ReU[i] + betU[i]*rfm.ReUdD[i][j]
    # Term 1: \beta^j B^i_{,j}
    for i in range(DIM):
        for j in range(DIM):
            B_rhsU[i] += betaU[j]*BU_dupD[i][j]
    # Term 2: \beta^j \bar{\Gamma}^i_{mj} B^m
    for i in range(DIM):
        for j in range(DIM):
            for m in range(DIM):
                B_rhsU[i] += betaU[j]*GammabarUDD[i][m][j]*BU[m]
    # Term 3: \frac{3}{4}\partial_t \bar{\Lambda}^{i}
    for i in range(DIM):
        B_rhsU[i] += sp.Rational(3,4)*Brhs.Lambdabar_rhsU[i]
    # Term 4: -\frac{3}{4}\beta^j \bar{\Lambda}^i_{,j}
    for i in range(DIM):
        for j in range(DIM):
            B_rhsU[i] += -sp.Rational(3,4)*betaU[j]*Brhs.LambdabarU_dupD[i][j]
    # Term 5: -\frac{3}{4}\beta^j \bar{\Gamma}^i_{mj} \bar{\Lambda}^m
    for i in range(DIM):
        for j in range(DIM):
            for m in range(DIM):
                B_rhsU[i] += -sp.Rational(3,4)*betaU[j]*GammabarUDD[i][m][j]*Bq.LambdabarU[m]
    # Term 6: - \eta B^i
    # eta is a free parameter; we declare it here:
    eta = par.Cparameters("REAL", thismodule, ["eta"],2.0)
    for i in range(DIM):
        B_rhsU[i] += -eta*BU[i]
```

# Step 4: Rescale right-hand sides of BSSN gauge equations \[Back to [top](#toc)\]
$$\label{rescale}$$

Next we rescale the right-hand sides of the BSSN equations so that the evolved variables are $\left\{h_{i j},a_{i j},\text{cf}, K, \lambda^{i}, \alpha, \mathcal{V}^i, \mathcal{B}^i\right\}$

```python
# Step 4: Rescale the BSSN gauge RHS quantities so that the evolved
#         variables may remain smooth across coord singularities
vet_rhsU    = ixp.zerorank1()
bet_rhsU    = ixp.zerorank1()
for i in range(DIM):
    vet_rhsU[i]    =   beta_rhsU[i] / rfm.ReU[i]
    bet_rhsU[i]    =      B_rhsU[i] / rfm.ReU[i]
#print(str(Abar_rhsDD[2][2]).replace("**","^").replace("_","").replace("xx","x").replace("sin(x2)","Sin[x2]").replace("sin(2*x2)","Sin[2*x2]").replace("cos(x2)","Cos[x2]").replace("detgbaroverdetghat","detg"))
#print(str(Dbarbetacontraction).replace("**","^").replace("_","").replace("xx","x").replace("sin(x2)","Sin[x2]").replace("detgbaroverdetghat","detg"))
#print(betaU_dD)
#print(str(trK_rhs).replace("xx2","xx3").replace("xx1","xx2").replace("xx0","xx1").replace("**","^").replace("_","").replace("sin(xx2)","Sinx2").replace("xx","x").replace("sin(2*x2)","Sin2x2").replace("cos(x2)","Cosx2").replace("detgbaroverdetghat","detg"))
#print(str(bet_rhsU[0]).replace("xx2","xx3").replace("xx1","xx2").replace("xx0","xx1").replace("**","^").replace("_","").replace("sin(xx2)","Sinx2").replace("xx","x").replace("sin(2*x2)","Sin2x2").replace("cos(x2)","Cosx2").replace("detgbaroverdetghat","detg"))
```

# Step 5: Code Validation against `BSSN.BSSN_gauge_RHSs` NRPy+ module \[Back to [top](#toc)\]
$$\label{code_validation}$$

Here, as a code validation check, we verify agreement in the SymPy expressions for the RHSs of the BSSN gauge equations between

1. this tutorial and
2. the NRPy+ [BSSN.BSSN_gauge_RHSs](../edit/BSSN/BSSN_gauge_RHSs.py) module.

By default, we analyze the RHSs in Spherical coordinates and with the covariant Gamma-driving second-order shift condition, though other coordinate systems & gauge conditions may be chosen.

```python
# Step 5: We already have SymPy expressions for BSSN gauge RHS expressions
#         in terms of other SymPy variables. Even if we reset the
#         list of NRPy+ gridfunctions, these *SymPy* expressions for
#         BSSN RHS variables *will remain unaffected*.
#
#         Here, we will use the above-defined BSSN gauge RHS expressions
#         to validate against the same expressions in the
#         BSSN/BSSN_gauge_RHSs.py file, to ensure consistency between
#         this tutorial and the module itself.
#
# Reset the list of gridfunctions, as registering a gridfunction
#   twice will spawn an error.
gri.glb_gridfcs_list = []

# Step 5.a: Call the BSSN_gauge_RHSs() function from within the
#           BSSN/BSSN_gauge_RHSs.py module,
#           which should generate exactly the same expressions as above.
import BSSN.BSSN_gauge_RHSs as Bgrhs
par.set_parval_from_str("BSSN.BSSN_gauge_RHSs::ShiftEvolutionOption","GammaDriving2ndOrder_Covariant")
Bgrhs.BSSN_gauge_RHSs()

print("Consistency check between BSSN.BSSN_gauge_RHSs tutorial and NRPy+ module: ALL SHOULD BE ZERO.")

print("alpha_rhs - bssnrhs.alpha_rhs = " + str(alpha_rhs - Bgrhs.alpha_rhs))

for i in range(DIM):
    print("vet_rhsU["+str(i)+"] - bssnrhs.vet_rhsU["+str(i)+"] = " + str(vet_rhsU[i] - Bgrhs.vet_rhsU[i]))
    print("bet_rhsU["+str(i)+"] - bssnrhs.bet_rhsU["+str(i)+"] = " + str(bet_rhsU[i] - Bgrhs.bet_rhsU[i]))
```

    Consistency check between BSSN.BSSN_gauge_RHSs tutorial and NRPy+ module: ALL SHOULD BE ZERO.
    alpha_rhs - bssnrhs.alpha_rhs = 0
    vet_rhsU[0] - bssnrhs.vet_rhsU[0] = 0
    bet_rhsU[0] - bssnrhs.bet_rhsU[0] = 0
    vet_rhsU[1] - bssnrhs.vet_rhsU[1] = 0
    bet_rhsU[1] - bssnrhs.bet_rhsU[1] = 0
    vet_rhsU[2] - bssnrhs.vet_rhsU[2] = 0
    bet_rhsU[2] - bssnrhs.bet_rhsU[2] = 0

# Step 6: Output this notebook to $\LaTeX$-formatted PDF file \[Back to [top](#toc)\]
$$\label{latex_pdf_output}$$

The following code cell converts this Jupyter notebook into a proper, clickable $\LaTeX$-formatted PDF file. After the cell is successfully run, the generated PDF may be found in the root NRPy+ tutorial directory, with filename
[Tutorial-BSSN_time_evolution-BSSN_gauge_RHSs.pdf](Tutorial-BSSN_time_evolution-BSSN_gauge_RHSs.pdf) (Note that clicking on this link may not work; you may need to open the PDF file through another means.)

```python
!jupyter nbconvert --to latex --template latex_nrpy_style.tplx --log-level='WARN' Tutorial-BSSN_time_evolution-BSSN_gauge_RHSs.ipynb
!pdflatex -interaction=batchmode Tutorial-BSSN_time_evolution-BSSN_gauge_RHSs.tex
!pdflatex -interaction=batchmode Tutorial-BSSN_time_evolution-BSSN_gauge_RHSs.tex
!pdflatex -interaction=batchmode Tutorial-BSSN_time_evolution-BSSN_gauge_RHSs.tex
!rm -f Tut*.out Tut*.aux Tut*.log
```

    [NbConvertApp] Converting notebook Tutorial-BSSN_time_evolution-BSSN_gauge_RHSs.ipynb to latex
    [NbConvertApp] Writing 65846 bytes to Tutorial-BSSN_time_evolution-BSSN_gauge_RHSs.tex
    This is pdfTeX, Version 3.14159265-2.6-1.40.18 (TeX Live 2017/Debian) (preloaded format=pdflatex)
     restricted \write18 enabled.
    entering extended mode
    This is pdfTeX, Version 3.14159265-2.6-1.40.18 (TeX Live 2017/Debian) (preloaded format=pdflatex)
     restricted \write18 enabled.
    entering extended mode
    This is pdfTeX, Version 3.14159265-2.6-1.40.18 (TeX Live 2017/Debian) (preloaded format=pdflatex)
     restricted \write18 enabled.
    entering extended mode


# LaTeX to medium

Being a dedicated user of jupyter notebooks in my research, I was very pleased to hear about the
[jupyter_to_medium](https://github.com/dexplo/jupyter_to_medium) package which is also introduced in this [post](https://medium.com/dunder-data/jupyter-to-medium-initial-post-ecd140d339f0).

I am very impressed how my work in jupyter can easily be converted into a medium post. One pretty essential part missing in the conversion however, are the LaTeX equations from my notebooks... :(

This is a known issue, but someone proposed a solution where matplotlib could be used to render the equations into images stored within the notebooks. This means that the equations will be converted to medium by [jupyter_to_medium](https://github.com/dexplo/jupyter_to_medium). Here is my attempt to use this solution.

The example notebook that was used to create this post is available at:
* [Kaggle](https://www.kaggle.com/martinlarsalbert/latex-to-medium)
* [GitHub](https://github.com/martinlarsalbert/kaggle/tree/main/kernels/latex_to_medium/latex_to_medium.ipynb)

I wrote a simple method that uses matplotlib to render the equations:

```python
import matplotlib.pyplot as plt
import sympy as sp

def display(eq:sp.Eq, size=20):
    """
    Use matplotlib to render LaTeX equations to figure within notebook.

    ----------
    eq : str or sympy.Eq
    LaTeX equation to render

    size : int
    size of the equation
    """

    if isinstance(eq, str):
        eq_latex = eq
    else:
        eq_latex = sp.latex(eq)

    fig,ax=plt.subplots()
    fig.set_size_inches(0.01, 0.01)
    ax.axis("off")
    plt.title(r'$' + eq_latex + r'$' , size=size);
```

As I'm also a big fan of the SymPy package, this method accepts equations both as pure LaTeX string or a SymPy expression.It is either possible to write the LaTeX:

```python
eq = r'E = m*c^2'
display(eq)
```

...or as a SymPy expression

```python
E,m,c = sp.symbols('E m c')
eq2 = sp.Eq(E,m*c**2)
display(eq2)
```

## Conclusions
Eventhough this requires that you rewrite your notebooks using the display method, I think that this is still much more convenient than having to create these LaTeX equations as images in some other (more manual way). I will now definatelly used the [jupyter_to_medium](https://github.com/dexplo/jupyter_to_medium) package for my next post.


## Histograms of Oriented Gradients (HOG)

As we saw with the ORB algorithm, we can use keypoints in images to do keypoint-based matching to detect objects in images. These type of algorithms work great when you want to detect objects that have a lot of consistent internal features that are not affected by the background. For example, these algorithms work well for facial detection because faces have a lot of consistent internal features that don’t get affected by the image background, such as the eyes, nose, and mouth. However, these type of algorithms don’t work so well when attempting to do more general object recognition, say for example, pedestrian detection in images. The reason is that people don’t have consistent internal features, like faces do, because the body shape and style of every person is different (see Fig. 1). This means that every person is going to have a different set of internal features, and so we need something that can more generally describe a person.

Fig. 1. - Pedestrians.

One option is to try to detect pedestrians by their contours instead.  Detecting objects in images by their contours (boundaries) is very challenging because we have to deal with the difficulties brought about by the contrast between the background and the foreground. For example, suppose you wanted to detect a pedestrian in an image that is walking in front of a white building and she is wearing a white coat and black pants (see Fig. 2). We can see in Fig. 2, that since the background of the image is mostly white, the black pants are going to have a very high contrast, but the coat, since it is white as well, is going to have very low contrast. In this case, detecting the edges of pants is going to be easy but detecting the edges of the coat is going to be very difficult. This is where **HOG** comes in. HOG stands for **Histograms of Oriented Gradients** and it was first introduced by Navneet Dalal and Bill Triggs in 2005.

Fig. 2. - High and Low Contrast.

The HOG algorithm works by creating histograms of the distribution of gradient orientations in an image and then normalizing them in a very special way. This special normalization is what makes HOG so effective at detecting the edges of objects even in cases where the contrast is very low. These normalized histograms are put together into a feature vector, known as the HOG descriptor, that can be used to train a machine learning algorithm, such as a Support Vector Machine (SVM), to detect objects in images based on their boundaries (edges). Due to its great success and reliability, HOG has become one of the most widely used algorithms in computer vison for object detection.

In this notebook, you will learn:

* How the HOG algorithm works
* How to use OpenCV to create a HOG descriptor
* How to visualize the HOG descriptor.

# The HOG Algorithm

As its name suggests, the HOG algorithm, is based on creating histograms from the orientation of image gradients. The HOG algorithm is implemented in a series of steps:

1. Given the image of particular object, set a detection window (region of interest) that covers the entire object in the image (see Fig. 3).

2. Calculate the magnitude and direction of the gradient for each individual pixel in the detection window.

3. Divide the detection window into connected *cells* of pixels, with all cells being of the same size (see Fig. 3). The size of the cells is a free parameter and it is usually chosen so as to match the scale of the features that want to be detected. For example, in a 64 x 128 pixel detection window, square cells 6 to 8 pixels wide are suitable for detecting human limbs.

4. Create a Histogram for each cell, by first grouping the gradient directions of all pixels in each cell into a particular number of orientation (angular) bins; and then adding up the gradient magnitudes of the gradients in each angular bin (see Fig. 3). The number of bins in the histogram is a free parameter and it is usually set to 9 angular bins.

5. Group adjacent cells into *blocks* (see Fig. 3). The number of cells in each block is a free parameter and all blocks must be of the same size. The distance between each block (known as the stride) is a free parameter but it is usually set to half the block size, in which case you will get overlapping blocks (*see video below*).  The HOG algorithm has been shown empirically to work better with overlapping blocks.

6. Use the cells contained within each block to normalize the cell histograms in that block (see Fig. 3). If you have overlapping blocks this means that most cells will be normalized with respect to different blocks (*see video below*). Therefore, the same cell may have several different normalizations.

7. Collect all the normalized histograms from all the blocks into a single feature vector called the HOG descriptor.

8. Use the resulting HOG descriptors from many images of the same type of object to train a machine learning algorithm, such as an SVM, to detect those type of objects in images. For example, you could use the HOG descriptors from many images of pedestrians to train an SVM to detect pedestrians in images. The training is done with both positive a negative examples of the object you want detect in the image.

9. Once the SVM has been trained, a sliding window approach is used to try to detect and locate objects in images.  Detecting an object in the image entails finding the part of the image that looks similar to the HOG pattern learned by the SVM.

Fig. 3. - HOG Diagram.

Vid. 1. - HOG Animation.

# Why The HOG Algorithm Works

As we learned above, HOG creates histograms by adding the magnitude of the gradients in particular orientations in localized portions of the image called *cells*.  By doing this we guarantee that stronger gradients will contribute more to the magnitude of their respective angular bin, while the effects of weak and randomly oriented gradients resulting from noise are minimized. In this manner the histograms tell us the dominant gradient orientation of each cell.

### Dealing with contrast

Now, the magnitude of the dominant orientation can vary widely due to variations in local illumination and the contrast between the background and the foreground.

To account for the background-foreground contrast differences, the HOG algorithm tries to detect edges locally.  In order to do this, it defines groups of cells, called **blocks**, and normalizes the histograms using this local group of cells. By normalizing locally, the HOG algorithm can detect the edges in each block very reliably; this is called **block normalization**.

In addition to using block normalization, the HOG algorithm also uses overlapping blocks to increase its performance. By using overlapping blocks, each cell contributes several independent components to the final HOG descriptor, where each component corresponds to a cell being normalized with respect to a different block. This may seem redundant but, it has been shown empirically that by normalizing each cell several times with respect to different local blocks, the performance of the HOG algorithm increases dramatically.

### Loading Images and Importing Resources

The first step in building our HOG descriptor is to load the required packages into Python and to load our image.

We start by using OpenCV to load an image of a triangle tile. Since, the `cv2.imread()` function loads images as BGR we will convert our image to RGB so we can display it with the correct colors. As usual we will convert our BGR image to Gray Scale for analysis.

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

# Set the default figure size
plt.rcParams['figure.figsize'] = [17.0, 7.0]

# Load the image
image = cv2.imread('./images/triangle_tile.jpeg')

# Convert the original image to RGB
original_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

# Convert the original image to gray scale
gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

# Print the shape of the original and gray scale images
print('The original image has shape: ', original_image.shape)
print('The gray scale image has shape: ', gray_image.shape)

# Display the images
plt.subplot(121)
plt.imshow(original_image)
plt.title('Original Image')
plt.subplot(122)
plt.imshow(gray_image, cmap='gray')
plt.title('Gray Scale Image')
plt.show()
```

# Creating The HOG Descriptor

We will be using OpenCV’s `HOGDescriptor` class to create the HOG descriptor. The parameters of the HOG descriptor are setup using the `HOGDescriptor()` function. The parameters of the `HOGDescriptor()` function and their default values are given below:

`cv2.HOGDescriptor(win_size = (64, 128),
                  block_size = (16, 16),
                  block_stride = (8, 8),
                  cell_size = (8, 8),
                  nbins = 9,
                  win_sigma = DEFAULT_WIN_SIGMA,
                  threshold_L2hys = 0.2,
                  gamma_correction = true,
                  nlevels = DEFAULT_NLEVELS)`

Parameters:

* **win_size** – *Size*
Size of detection window in pixels (*width, height*). Defines the region of interest. Must be an integer multiple of cell size.

* **block_size** – *Size*
Block size in pixels (*width, height*). Defines how many cells are in each block. Must be an integer multiple of cell size and it must be smaller than the detection window. The smaller the block the finer detail you will get.

* **block_stride** – *Size*
Block stride in pixels (*horizontal, vertical*). It must be an integer multiple of cell size. The `block_stride` defines the distance between adjecent blocks, for example, 8 pixels horizontally and 8 pixels vertically. Longer `block_strides` makes the algorithm run faster (because less blocks are evaluated) but the algorithm may not perform as well.

* **cell_size** – *Size*
Cell size in pixels (*width, height*). Determines the size fo your cell. The smaller the cell the finer detail you will get.

* **nbins** – *int*
Number of bins for the histograms. Determines the number of angular bins used to make the histograms. With more bins you capture more gradient directions. HOG uses unsigned gradients, so the angular bins will have values between 0 and 180 degrees.

* **win_sigma** – *double*
Gaussian smoothing window parameter. The performance of the HOG algorithm can be improved by smoothing the pixels near the edges of the blocks by applying a Gaussian spatial window to each pixel before computing the histograms.

* **threshold_L2hys** – *double*
L2-Hys (Lowe-style clipped L2 norm) normalization method shrinkage. The L2-Hys method is used to normalize the blocks and it consists of an L2-norm followed by clipping and a renormalization. The clipping limits the maximum value of the descriptor vector for each block to have the value of the given threshold (0.2 by default). After the clipping the descriptor vector is renormalized as described in *IJCV*, 60(2):91-110, 2004.

* **gamma_correction** – *bool*
Flag to specify whether the gamma correction preprocessing is required or not. Performing gamma correction slightly increases the performance of the HOG algorithm.

* **nlevels** – *int*
Maximum number of detection window increases.

As we can see, the  `cv2.HOGDescriptor()`function supports a wide range of parameters. The first few arguments (`block_size, block_stride, cell_size`, and `nbins`) are probably the ones you are most likely to change. The other parameters can be safely left at their default values and you will get good results.

In the code below, we will use the `cv2.HOGDescriptor()`function to set the cell size, block size, block stride, and the number of bins for the histograms of the HOG descriptor. We will then use `.compute(image)`method to compute the HOG descriptor (feature vector) for the given `image`.

```python
# Specify the parameters for our HOG descriptor

# Cell Size in pixels (width, height). Must be smaller than the size of the detection window
# and must be chosen so that the resulting Block Size is smaller than the detection window.
cell_size = (6, 6)

# Number of cells per block in each direction (x, y). Must be chosen so that the resulting
# Block Size is smaller than the detection window
num_cells_per_block = (2, 2)

# Block Size in pixels (width, height). Must be an integer multiple of Cell Size.
# The Block Size must be smaller than the detection window
block_size = (num_cells_per_block[0] * cell_size[0],
              num_cells_per_block[1] * cell_size[1])

# Calculate the number of cells that fit in our image in the x and y directions
x_cells = gray_image.shape[1] // cell_size[0]
y_cells = gray_image.shape[0] // cell_size[1]

# Horizontal distance between blocks in units of Cell Size. Must be an integer and it must
# be set such that (x_cells - num_cells_per_block[0]) / h_stride = integer.
h_stride = 1

# Vertical distance between blocks in units of Cell Size. Must be an integer and it must
# be set such that (y_cells - num_cells_per_block[1]) / v_stride = integer.
v_stride = 1

# Block Stride in pixels (horizantal, vertical). Must be an integer multiple of Cell Size
block_stride = (cell_size[0] * h_stride, cell_size[1] * v_stride)

# Number of gradient orientation bins
num_bins = 9

# Specify the size of the detection window (Region of Interest) in pixels (width, height).
# It must be an integer multiple of Cell Size and it must cover the entire image. Because
# the detection window must be an integer multiple of cell size, depending on the size of
# your cells, the resulting detection window might be slightly smaller than the image.
# This is perfectly ok.
win_size = (x_cells * cell_size[0] , y_cells * cell_size[1])

# Print the shape of the gray scale image for reference
print('\nThe gray scale image has shape: ', gray_image.shape)
print()

# Print the parameters of our HOG descriptor
print('HOG Descriptor Parameters:\n')
print('Window Size:', win_size)
print('Cell Size:', cell_size)
print('Block Size:', block_size)
print('Block Stride:', block_stride)
print('Number of Bins:', num_bins)
print()

# Set the parameters of the HOG descriptor using the variables defined above
hog = cv2.HOGDescriptor(win_size, block_size, block_stride, cell_size, num_bins)

# Compute the HOG Descriptor for the gray scale image
hog_descriptor = hog.compute(gray_image)
```

    The gray scale image has shape:  (250, 250)

    HOG Descriptor Parameters:

    Window Size: (246, 246)
    Cell Size: (6, 6)
    Block Size: (12, 12)
    Block Stride: (6, 6)
    Number of Bins: 9

# Number of Elements In The HOG Descriptor

The resulting HOG Descriptor (feature vector), contains the normalized histograms from all cells from all blocks in the detection window concatenated in one long vector. Therefore, the size of the HOG feature vector will be given by the total number of blocks in the detection window, multiplied by the number of cells per block, times the number of orientation bins:

\begin{equation}
\mbox{total_elements} = (\mbox{total_number_of_blocks})\mbox{ } \times \mbox{ } (\mbox{number_cells_per_block})\mbox{ } \times \mbox{ } (\mbox{number_of_bins})
\end{equation}

If we don’t have overlapping blocks (*i.e.* the `block_stride`equals the `block_size`), the total number of blocks can be easily calculated by dividing the size of the detection window by the block size. However, in the general case we have to take into account the fact that we have overlapping blocks. To find the total number of blocks in the general case (*i.e.* for any `block_stride` and `block_size`), we can use the formula given below:

\begin{equation}
\mbox{Total}_i = \left( \frac{\mbox{block_size}_i}{\mbox{block_stride}_i} \right)\left( \frac{\mbox{window_size}_i}{\mbox{block_size}_i} \right) - \left [\left( \frac{\mbox{block_size}_i}{\mbox{block_stride}_i} \right) - 1 \right]; \mbox{  for  } i = x,y
\end{equation}

Where Total$_x$, is the total number of blocks along the width of the detection window, and Total$_y$, is the total number of blocks along the height of the detection window. This formula for Total$_x$ and Total$_y$, takes into account the extra blocks that result from overlapping. After calculating Total$_x$ and Total$_y$, we can get the total number of blocks in the detection window by multiplying Total$_x$  $\times$ Total$_y$. The above formula can be simplified considerably because the `block_size`, `block_stride`, and `window_size`are all defined in terms of the `cell_size`. By making all the appropriate substitutions and cancelations the above formula reduces to:

\begin{equation}
\mbox{Total}_i = \left(\frac{\mbox{cells}_i - \mbox{num_cells_per_block}_i}{N_i}\right) + 1\mbox{  }; \mbox{  for  } i = x,y
\end{equation}

Where cells$_x$ is the total number of cells along the width of the detection window, and cells$_y$, is the total number of cells along the height of the detection window. And $N_x$ is the horizontal block stride in units of `cell_size` and $N_y$ is the vertical block stride in units of `cell_size`.

Let's calculate what the number of elements for the HOG feature vector should be and check that it matches the shape of the HOG Descriptor calculated above.

```python
# Calculate the total number of blocks along the width of the detection window
tot_bx = np.uint32(((x_cells - num_cells_per_block[0]) / h_stride) + 1)

# Calculate the total number of blocks along the height of the detection window
tot_by = np.uint32(((y_cells - num_cells_per_block[1]) / v_stride) + 1)

# Calculate the total number of elements in the feature vector
tot_els = (tot_bx) * (tot_by) * num_cells_per_block[0] * num_cells_per_block[1] * num_bins

# Print the total number of elements the HOG feature vector should have
print('\nThe total number of elements in the HOG Feature Vector should be: ',
      tot_bx, 'x',
      tot_by, 'x',
      num_cells_per_block[0], 'x',
      num_cells_per_block[1], 'x',
      num_bins, '=',
      tot_els)

# Print the shape of the HOG Descriptor to see that it matches the above
print('\nThe HOG Descriptor has shape:', hog_descriptor.shape)
print()
```

# Visualizing The HOG Descriptor

We can visualize the HOG Descriptor by plotting the histogram associated with each cell as a collection of vectors. To do this, we will plot each bin in the histogram as a single vector whose magnitude is given by the height of the bin and its orientation is given by the angular bin that its associated with. Since any given cell might have multiple histograms associated with it, due to the overlapping blocks, we will choose to average all the histograms for each cell to produce a single histogram for each cell.

OpenCV has no easy way to visualize the HOG Descriptor, so we have to do some manipulation first in order to visualize it. We will start by reshaping the HOG Descriptor in order to make our calculations easier.  We will then compute the average histogram of each cell and finally we will convert the histogram bins into vectors. Once we have the vectors, we plot the corresponding vectors for each cell in an image.

The code below produces an interactive plot so that you can interact with the figure. The figure contains:
* the grayscale image,
* the HOG Descriptor (feature vector),
* a zoomed-in portion of the HOG Descriptor, and
* the histogram of the selected cell.

**You can click anywhere on the gray scale image or the HOG Descriptor image to select a particular cell**. Once you click on either image a *magenta* rectangle will appear showing the cell you selected. The Zoom Window will show you a zoomed in version of the HOG descriptor around the selected cell; and the histogram plot will show you the corresponding histogram for the selected cell. The interactive window also has buttons at the bottom that allow for other functionality, such as panning, and giving you the option to save the figure if desired. The home button returns the figure to its default value.

**NOTE**: If you are running this notebook in the Udacity workspace, there is around a 2 second lag in the interactive plot. This means that if you click in the image to zoom in, it will take about 2 seconds for the plot to refresh.

```python
%matplotlib notebook

import copy
import matplotlib.patches as patches

# Set the default figure size
plt.rcParams['figure.figsize'] = [9.8, 9]

# Reshape the feature vector to [blocks_y, blocks_x, num_cells_per_block_x, num_cells_per_block_y, num_bins].
# The blocks_x and blocks_y will be transposed so that the first index (blocks_y) referes to the row number
# and the second index to the column number. This will be useful later when we plot the feature vector, so
# that the feature vector indexing matches the image indexing.
hog_descriptor_reshaped = hog_descriptor.reshape(tot_bx,
                                                 tot_by,
                                                 num_cells_per_block[0],
                                                 num_cells_per_block[1],
                                                 num_bins).transpose((1, 0, 2, 3, 4))

# Print the shape of the feature vector for reference
print('The feature vector has shape:', hog_descriptor.shape)

# Print the reshaped feature vector
print('The reshaped feature vector has shape:', hog_descriptor_reshaped.shape)

# Create an array that will hold the average gradients for each cell
ave_grad = np.zeros((y_cells, x_cells, num_bins))

# Print the shape of the ave_grad array for reference
print('The average gradient array has shape: ', ave_grad.shape)

# Create an array that will count the number of histograms per cell
hist_counter = np.zeros((y_cells, x_cells, 1))

# Add up all the histograms for each cell and count the number of histograms per cell
for i in range (num_cells_per_block[0]):
    for j in range(num_cells_per_block[1]):
        ave_grad[i:tot_by + i,
                 j:tot_bx + j] += hog_descriptor_reshaped[:, :, i, j, :]

        hist_counter[i:tot_by + i,
                     j:tot_bx + j] += 1

# Calculate the average gradient for each cell
ave_grad /= hist_counter

# Calculate the total number of vectors we have in all the cells.
len_vecs = ave_grad.shape[0] * ave_grad.shape[1] * ave_grad.shape[2]

# Create an array that has num_bins equally spaced between 0 and 180 degress in radians.
deg = np.linspace(0, np.pi, num_bins, endpoint = False)

# Each cell will have a histogram with num_bins. For each cell, plot each bin as a vector (with its magnitude
# equal to the height of the bin in the histogram, and its angle corresponding to the bin in the histogram).
# To do this, create rank 1 arrays that will hold the (x,y)-coordinate of all the vectors in all the cells in the
# image. Also, create the rank 1 arrays that will hold all the (U,V)-components of all the vectors in all the
# cells in the image. Create the arrays that will hold all the vector positons and components.
U = np.zeros((len_vecs))
V = np.zeros((len_vecs))
X = np.zeros((len_vecs))
Y = np.zeros((len_vecs))

# Set the counter to zero
counter = 0

# Use the cosine and sine functions to calculate the vector components (U,V) from their maginitudes. Remember the
# cosine and sine functions take angles in radians. Calculate the vector positions and magnitudes from the
# average gradient array
for i in range(ave_grad.shape[0]):
    for j in range(ave_grad.shape[1]):
        for k in range(ave_grad.shape[2]):
            U[counter] = ave_grad[i,j,k] * np.cos(deg[k])
            V[counter] = ave_grad[i,j,k] * np.sin(deg[k])

            X[counter] = (cell_size[0] / 2) + (cell_size[0] * i)
            Y[counter] = (cell_size[1] / 2) + (cell_size[1] * j)

            counter = counter + 1

# Create the bins in degress to plot our histogram.
angle_axis = np.linspace(0, 180, num_bins, endpoint = False)
angle_axis += ((angle_axis[1] - angle_axis[0]) / 2)

# Create a figure with 4 subplots arranged in 2 x 2
fig, ((a,b),(c,d)) = plt.subplots(2,2)

# Set the title of each subplot
a.set(title = 'Gray Scale Image\n(Click to Zoom)')
b.set(title = 'HOG Descriptor\n(Click to Zoom)')
c.set(title = 'Zoom Window', xlim = (0, 18), ylim = (0, 18), autoscale_on = False)
d.set(title = 'Histogram of Gradients')

# Plot the gray scale image
a.imshow(gray_image, cmap = 'gray')
a.set_aspect(aspect = 1)

# Plot the feature vector (HOG Descriptor)
b.quiver(Y, X, U, V, color = 'white', headwidth = 0, headlength = 0, scale_units = 'inches', scale = 5)
b.invert_yaxis()
b.set_aspect(aspect = 1)
b.set_facecolor('black')

# Define function for interactive zoom
def onpress(event):

    #Unless the left mouse button is pressed do nothing
    if event.button != 1:
        return

    # Only accept clicks for subplots a and b
    if event.inaxes in [a, b]:

        # Get mouse click coordinates
        x, y = event.xdata, event.ydata

        # Select the cell closest to the mouse click coordinates
        cell_num_x = np.uint32(x / cell_size[0])
        cell_num_y = np.uint32(y / cell_size[1])

        # Set the edge coordinates of the rectangle patch
        edgex = x - (x % cell_size[0])
        edgey = y - (y % cell_size[1])

        # Create a rectangle patch that matches the the cell selected above
        rect = patches.Rectangle((edgex, edgey),
                                  cell_size[0], cell_size[1],
                                  linewidth = 1,
                                  edgecolor = 'magenta',
                                  facecolor='none')

        # A single patch can only be used in a single plot. Create copies
        # of the patch to use in the other subplots
        rect2 = copy.copy(rect)
        rect3 = copy.copy(rect)

        # Update all subplots
        a.clear()
        a.set(title = 'Gray Scale Image\n(Click to Zoom)')
        a.imshow(gray_image, cmap = 'gray')
        a.set_aspect(aspect = 1)
        a.add_patch(rect)

        b.clear()
        b.set(title = 'HOG Descriptor\n(Click to Zoom)')
        b.quiver(Y, X, U, V, color = 'white', headwidth = 0, headlength = 0, scale_units = 'inches', scale = 5)
        b.invert_yaxis()
        b.set_aspect(aspect = 1)
        b.set_facecolor('black')
        b.add_patch(rect2)

        c.clear()
        c.set(title = 'Zoom Window')
        c.quiver(Y, X, U, V, color = 'white', headwidth = 0, headlength = 0, scale_units = 'inches', scale = 1)
        c.set_xlim(edgex - cell_size[0], edgex + (2 * cell_size[0]))
        c.set_ylim(edgey - cell_size[1], edgey + (2 * cell_size[1]))
        c.invert_yaxis()
        c.set_aspect(aspect = 1)
        c.set_facecolor('black')
        c.add_patch(rect3)

        d.clear()
        d.set(title = 'Histogram of Gradients')
        d.grid()
        d.set_xlim(0, 180)
        d.set_xticks(angle_axis)
        d.set_xlabel('Angle')
        d.bar(angle_axis,
              ave_grad[cell_num_y, cell_num_x, :],
              180 // num_bins,
              align = 'center',
              alpha = 0.5,
              linewidth = 1.2,
              edgecolor = 'k')

        fig.canvas.draw()

# Create a connection between the figure and the mouse click
fig.canvas.mpl_connect('button_press_event', onpress)
plt.show()
```

# Understanding The Histograms

Let's take a look at a couple of snapshots of the above figure to see if the histograms for the selected cell make sense. Let's start looking at a cell that is inside a triangle and not near an edge:

Fig. 4. - Histograms Inside a Triangle.

In this case, since the triangle is nearly all of the same color there shouldn't be any dominant gradient in the selected cell. As we can clearly see in the Zoom Window and the histogram, this is indeed the case. We have many gradients but none of them clearly dominates over the other.

Now let’s take a look at a cell that is near a horizontal edge:

Fig. 5. - Histograms Near a Horizontal Edge.

Remember that edges are areas of an image where the intensity changes abruptly. In these cases, we will have a high intensity gradient in some particular direction. This is exactly what we see in the corresponding histogram and Zoom Window for the selected cell.  In the Zoom Window, we can see that the dominant gradient is pointing up, almost at 90 degrees, since that’s the direction in which there is a sharp change in intensity. Therefore, we should expect to see the 90-degree bin in the histogram to dominate strongly over the others. This is in fact what we see.

Now let’s take a look at a cell that is near a vertical edge:

Fig. 6. - Histograms Near a Vertical Edge.

In this case we expect the dominant gradient in the cell to be horizontal, close to 180 degrees, since that’s the direction in which there is a sharp change in intensity. Therefore, we should expect to see the 170-degree bin in the histogram to dominate strongly over the others. This is what we see in the histogram but we also see that there is another dominant gradient in the cell, namely the one in the 10-degree bin. The reason for this, is because the HOG algorithm is using unsigned gradients, which means 0 degrees and 180 degrees are considered the same. Therefore, when the histograms are being created, angles between 160 and 180 degrees, contribute proportionally to both the 10-degree bin and the 170-degree bin. This results in there being two dominant gradients in the cell near the vertical edge instead of just one.

To conclude let’s take a look at a cell that is near a diagonal edge.

Fig. 7. - Histograms Near a Diagonal Edge.

To understand what we are seeing, let’s first remember that gradients have an *x*-component, and a *y*-component, just like vectors. Therefore, the resulting orientation of a gradient is going to be given by the vector sum of its components. For this reason, on vertical edges the gradients are horizontal, because they only have an x-component, as we saw in Figure 4. While on horizontal edges the gradients are vertical, because they only have a y-component, as we saw in Figure 3. Consequently, on diagonal edges, the gradients are also going to be diagonal because both the *x* and *y* components are non-zero.  Since the diagonal edges in the image are close to 45 degrees, we should expect to see a dominant gradient orientation in the 50-degree bin. This is in fact what we see in the histogram but, just like in Figure 4., we see there are two dominant gradients instead of just one. The reason for this is that when the histograms are being created, angles that are near the boundaries of bins, contribute proportionally to the adjacent bins.  For example, a gradient with an angle of 40 degrees, is right in the middle of the 30-degree and 50-degree bin. Therefore, the magnitude of the gradient is split evenly into the 30-degree and 50-degree bin. This results in there being two dominant gradients in the cell near the diagonal edge instead of just one.

Now that you know how HOG is implemented, in the workspace you will find a notebook named *Examples*. In there, you will be able set your own paramters for the HOG descriptor for various images. Have fun!


```python
import ipywidgets as ipw
import json
import random
import time
import pandas as pd
import os
import webbrowser
import math
from IPython.display import display, Markdown

# set kinetic parameters
with open("rate_parameters.json") as infile:
    jsdata = json.load(infile)

params = jsdata["kin1"]
```

Copyright **Jacob Martin and Paolo Raiteri**, January 2021

## Numerical solution of chemical equilibrium problems #2
Sea water is a complex mixture of electrolytes and organic matter in dynamic equilibrium with each other.
Calcium and Magnesium are among the most abundant ion present in sea water and in this exercise we want to study the equilibrium between those two ion and sulphate.
Assuming all other species present in the solution have a negligible effect on the ion association, the two reactions we need to consider are

\begin{equation}
\mathrm{Ca^{2+}(aq) + SO_4^{2-}(aq) \rightleftharpoons CaSO_4(aq)} \qquad ; \qquad pK_{1} = -2.9
\end{equation}

and

\begin{equation}
\mathrm{Mg^{2+}(aq) + SO_4^{2-}(aq) \rightleftharpoons MgSO_4(aq)} \qquad ; \qquad pK_{2} = -3.5
\end{equation}

We can start again by constructing the *ICE* table

|      | [Ca]        | [Mg]       | [SO$_4^{2-}$]           | [CaSO$_4$]          |[MgSO$_4$]
| :--- | :--------:  |:---------: |:---------:              | :--:                | :--:
| *I*  | [Ca]$_0$    | [Mg]$_0$   | [SO$_4^{2-}$]$_0$       | [CaSO$_4$]$_0$      | [MgSO$_4$]$_0$
| *C1* | -x          |            | -x                      | +x                  |
| *C2* |             | -y         | -y                      |                     | +y
| *E*  | [Ca]$_0$-x  | [Mg]$_0$-y | [SO$_4^{2-}$]$_0$ -x -y | [CaSO$_4$]$_0$ +x   | [MgSO$_4$]$_0$ +y

Then, following the same procedure we devised in the previous exercise we can define the *driving forces* in the system using the logarithm of the ratio between the reaction quotient and the equilibrium constant

\begin{equation}
x = \delta\ln\bigg[\frac{K_{1}}{Q_1}\bigg] \qquad ; \qquad y = \delta\ln\bigg[\frac{K_{2}}{Q_2}\bigg]
\end{equation}

where,

\begin{equation}
Q_1 = \dfrac{\mathrm{[CaSO_4]}}{\mathrm{[Ca^{2+}][SO_4^{2-}]}} \qquad \mathrm{and} \qquad
Q_2 = \dfrac{\mathrm{[MgSO_4]}}{\mathrm{[Mg^{2+}][SO_4^{2-}]}}
\end{equation}

Using an excel spreadsheet that implements a minimisation procedure similar to the one used in the previous exercise you can now study what are the equilibrium concentrations of Ca$^{2+}$,  Mg$^{2+}$, SO$_4^{2-}$, CaSO$_4$ and MgSO$_4$ for any starting concentrations of the ions.

Note that this is a hypothetical exercise used for illustrative purposes only.
It would indeed resemble real experiments only for concetrations of the ions lower than mM concentrations.
In fact, at high concentration the solid sulphate phasese will precipitate out of solution.

- Click `Download CSV` to export the data as a CSV file to verify your result.

```python
def initialise():
    global nPoints
    global concA, concB, concC
    global Keq1, Keq2
    global delta

    nPoints = 20
    concA = 0.01
    concB = 0.01
    concC = 0.02
    Keq1 = math.pow(10,2.9)
    Keq2 = math.pow(10,3.5)
    delta = 0.1

def addLine(t,x,y,z,xz,yz,res,q1,q2):
    var_list = []
    var_list.append(t)
    var_list.append(x)
    var_list.append(y)
    var_list.append(z)
    var_list.append(xz)
    var_list.append(yz)
    var_list.append(q1)
    var_list.append(q2)
    res.loc[len(res)] = var_list

initialise()

```

```python
respath = os.path.join(os.getcwd(), "..", "results.csv")

out_P = ipw.Output()
out_L = ipw.Output()

with out_L:
    display(Markdown("[Download CSV](../results.csv)"))

def force(Q,k):
    if (abs(Q) > 1.e-6):
        force = - math.log(Q/k)
    else:
        force = 1.

    return force

def calc1(btn):
    out_P.clear_output()

    if os.path.exists(respath):
        os.remove(respath)
    res = pd.DataFrame(columns=["step" , "[Ca$^{2+}]" , "[Mg$^{2+}]", "[SO$_4^{2-}$]",
                                "[CaSO$_4$]", "[MgSO$_4$]", "$Q_1/K_1$", "$Q_2/K_2$"])

    A  = float(concA_text.value)
    B  = float(concB_text.value)
    C  = float(concC_text.value)
    AC = 0
    BC = 0
    dx = float(delta_text.value)
    k1 = float(Keq1_text.value)
    k2 = float(Keq2_text.value)
    n  = int(nPoints_text.value)

    Q1 = AC / A / C
    Q2 = BC / B / C

    addLine(0,A,B,C,AC,BC,res,Q1/k1,Q2/k2)

    for i in range(0, n):
        Q1 = AC / A / C
        Q2 = BC / B / C
        f1 = force(Q1,k1)
        f2 = force(Q2,k2)

        dc = min(A,B)
        dc = min(C,dc)
        dc = min(AC,dc)
        dc = min(BC,dc)
        if (dc == 0):
            dc = 0.0001

        f1 = f1 * dc
        f2 = f2 * dc

        A = A - dx * f1
        B = B - dx * f2
        C = C - dx * (f1 + f2)
        AC = AC + dx * f1
        BC = BC + dx * f2

        addLine(i,A,B,C,AC,BC,res,Q1/k1,Q2/k2)

    res.to_csv(respath, index=False)
    with out_P:
        display(res.tail(n))

btn_calc1 = ipw.Button(description="Get Data", layout=ipw.Layout(width="150px"))
btn_calc1.on_click(calc1)

rows = []

# Equilibrium constant
Keq1_text = ipw.Text(str(Keq1))
Keq2_text = ipw.Text(str(Keq2))

# Initial concentrations
concA_text = ipw.Text(str(concA))

concB_text = ipw.Text(str(concB))

concC_text = ipw.Text(str(concC))

# delta concentration
delta_text = ipw.Text(str(delta))

# Nmber of data points
nPoints_text = ipw.Text(str(nPoints))

rows.append(ipw.HBox([ipw.Label('Initial concentration of Ca$^{2+}$    :  '),concA_text]))
rows.append(ipw.HBox([ipw.Label('Initial concentration of Mg$^{2+}$    :  '),concB_text]))
rows.append(ipw.HBox([ipw.Label('Initial concentration of SO$_4^{2-}$  :  '),concC_text]))
rows.append(ipw.HBox([ipw.Label('Equilibrium constant ($K_1$)          :  '),Keq1_text]))
rows.append(ipw.HBox([ipw.Label('Equilibrium constant ($K_2$)          :  '),Keq2_text]))
rows.append(ipw.HBox([ipw.Label('Delta concentration                   :  '),delta_text]))
rows.append(ipw.HBox([ipw.Label('Number of data point required         :  '),nPoints_text]))

rows.append(ipw.HBox([btn_calc1]))

rows.append(ipw.HBox([out_L]))
rows.append(ipw.HBox([out_P]))

ipw.VBox(rows)
```

```python

```