# TDEM: Casing Integrity Example

- background resistivity average is 10 Ωm
- For each well we measure the radial electric fields as far out as the well is deep. We also measured magnetic fields (tangential) at a few points.
- Return Electrode: 1000m from the wellhead, perpendicular to the array of receivers (wire ran close to a straight line).
- freqs DC (1e-6), 0.1, and 1Hz
- Surface casing : outer diameter 10 3/4 in, thickness = 0.011m - set depth 1242 ft
- Production casing : outer diameter 7 in, production = 0.008m - set depth 6079 ft
- KB 13.5ft above

```python
import discretize
from discretize import utils
import numpy as np
import scipy.sparse as sp
import sympy
import matplotlib.pyplot as plt
from matplotlib.colors import LogNorm
from scipy.constants import mu_0, inch, foot

from SimPEG.EM import TDEM
from SimPEG import Utils, Maps

import casingSimulations

from pymatsolver import Pardiso

%matplotlib inline
```

```python
simDir = '1500m_well_10ohm_m_TDEM_airn5'
```

## Set up model parameters

```python
# Casing diameters
surface_casing_diameter = 10.75*inch
production_casing_diameter = 7*inch

# casing depths (below surface)
kb = 13.5
surface_casing_depth = (1242-kb)*foot
production_casing_depth = (6079-kb)*foot

print(surface_casing_depth, production_casing_depth)
print(surface_casing_diameter, production_casing_diameter)
```

    (374.44679999999994, 1848.7643999999998)
    (0.27305, 0.17779999999999999)

### Simplified model

```python
# Casing parameters
casing_d = 20e-2 # first pass, lets use a 20cm casing diameter
casing_t = 1e-2 # 1cm casing thickness
casing_l = 1000 # here we use a 1km well and will explore the impacts of meshing choices

sigma_back = 1e-1
sigma_inside = 1.

src_a = np.r_[0., np.pi, -975.]
src_b = np.r_[1e3, np.pi, 0.]
```

```python
# set up the casing properties
cp = casingSimulations.CasingParameters(
    directory=simDir,
    sigma_back = sigma_back,
    sigma_layer = sigma_back,
    sigma_inside = sigma_inside,
    sigma_air = 1e-5,
    casing_d = casing_d,
    casing_l = casing_l,
    casing_t = casing_t,
    src_a = src_a,
    src_b = src_b
#     mur_casing = 1.
)
```

```python
cp.serialize()
```

    {u'__class__': 'CasingParameters',
     'casing_d': 0.2,
     'casing_l': 1000.0,
     'casing_t': 0.01,
     'casing_top': 0.0,
     'directory': u'1500m_well_10ohm_m_TDEM_airn5',
     'filename': u'CasingParameters.json',
     'layer_z': [-1000.0, -900.0],
     'mur_casing': 100.0,
     'sigma_air': 1e-05,
     'sigma_back': 0.1,
     'sigma_casing': 5500000.0,
     'sigma_inside': 1.0,
     'sigma_layer': 0.1,
     'src_a': [0.0, 3.141592653589793, -975.0],
     'src_b': [1000.0, 3.141592653589793, 0.0],
     'version': u'0.0.1'}

## Set up mesh

```python
npadx, npadz = 11, 18
dx2 = casing_l  # go out as far as the casing is deep
csx2 = 25.
csz = 2.5
pfx2 = 1.4
pfz = 1.4

ncy = 1
nstretchy = 5
stretchfact = 1.5
hy = utils.meshTensor([(1, nstretchy, -stretchfact), (1, ncy), (1, nstretchy, stretchfact)])
hy = hy * 2*np.pi/hy.sum()

cylMeshGen = casingSimulations.CasingMeshGenerator(
    directory=simDir,
    cp=cp,
    npadx=npadx,
    npadz=npadz,
    domain_x2=dx2,
    hy=hy,
    csx2=csx2,
    csz=csz,
    pfx2=pfx2,
    pfz=pfz
)
```

```python
cp.diffusion_distance(t=2)
```

    5641.8958354775632

```python
cylMeshGen.mesh.plotGrid()
```

```python
print(
    'nC: {}, max_x: {}, max_z: {}'.format(
        cylMeshGen.mesh.nC, cylMeshGen.mesh.vectorNx.max(), cylMeshGen.mesh.vectorNz.max()
    )
)
```

    nC: 637780, max_x: 4555.86952346, max_z: 3738.93997434

```python
cylmesh = cylMeshGen.mesh
```

```python
src = casingSimulations.sources.DownHoleCasingSrc(cp=cp, meshGenerator=cylMeshGen, directory=simDir, physics="TDEM")
# src = casingSimulations.sources.TopCasingSrc(cp=cp, meshGenerator=cylMeshGen, directory=simDir, physics="TDEM")
```

```python
ax = plt.subplot(111, projection='polar')
cylmesh.plotGrid(ax=ax, slice='z')
ax.plot(cylmesh.gridFx[src.surface_wire,1], cylmesh.gridFx[src.surface_wire,0], 'ro')
ax.set_rlim([0., 1900])
```

```python
fig, ax = plt.subplots(1, 1)
cylmesh.plotGrid(ax=ax, slice='theta')
src.plot(ax=ax)

ax.set_xlim([00., 10.])
ax.set_ylim([-1000., 30.])
```

```python
physprops = casingSimulations.PhysicalProperties(cylmesh, cp)
```

```python
mesh2D = discretize.CylMesh([cylmesh.hx, 1., cylmesh.hz], x0 = cylmesh.x0)
```

```python
xlim = [0.0, 0.15]
ylim = [-1200., 10.]
fig, ax = plt.subplots(1, 2, figsize=(12, 8))

murplt = physprops.mur.reshape(cylmesh.vnC, order='F')

plt.colorbar(mesh2D.plotImage(utils.mkvc(murplt[:,0,:]), ax=ax[0], mirror=True)[0], ax=ax[0])
ax[0].set_xlim(xlim)
ax[0].set_ylim(ylim)
src.plot(ax=ax[0])

sigmaplt = physprops.sigma.reshape(cylmesh.vnC, order='F')

plt.colorbar(
    mesh2D.plotImage(
        utils.mkvc(sigmaplt[:,0,:]), ax=ax[1], mirror=True, pcolorOpts = {'norm':LogNorm()}
    )[0], ax=ax[1],

)
ax[1].set_xlim(xlim)
ax[1].set_ylim(ylim)

plt.tight_layout()
```

```python
cp.timeSteps = [(1e-6, 10), (1e-5, 20), (1e-4, 20), (1e-3, 40), (1e-2, 20)]
```

```python
# # write out simulation files
# cp.save()
# cylMeshGen.save()
# src.save()
```

```python
sim3D = casingSimulations.run.SimulationTDEM(
    directory=simDir,
    cp=cp,
    meshGenerator=cylMeshGen,
    src=src
)
```

```python
sim3D.write_py()
```

    Saved 1km_well_10ohm_m_TDEM_downhole/CasingParameters.json
    Saved 1km_well_10ohm_m_TDEM_downhole/MeshParameters.json
    Saved 1km_well_10ohm_m_TDEM_downhole/Source.json
    wrote 1km_well_10ohm_m_TDEM_downhole/simulation.py

```python
sim3D.prob.times.max()
```

    0.24221000000000009

## 2D simulation

```python
mesh2D = meshGen.copy()
mesh2D.hy = np.r_[2*np.pi]
```

```python
mesh2D.mesh.plotGrid()
```

```python
src2D = casingSimulations.sources.TopCasingSrc(cp=cp, meshGenerator=mesh2D, directory=simDir)
```

```python
src2D.plot()
```

```python

```

```python
sim2D = casingSimulations.run.SimulationTDEM(
    cp=cp,
    meshGenerator=mesh2D,
    src=src2D,
    directory=simDir,
    fields_filename='fields2D.npy',
    filename='simulation2D.json'
)
```

```python
fields2D = sim2D.run()
```

    Validating parameters...
          max x: 4555.86952346, min z: -4738.93997434, max z: 3738.93997434
    Saved 1km_well_10ohm_m_TDEM/simulation2D.json
    Starting Simulation
       ... Done. Elapsed time : 9.49255084991

## Solve DC for comparison

```python
from SimPEG.EM.Static import DC
```

```python
src_a = meshGen.mesh.gridFz[sim3D.src.tophole_electrode, :][0, :] - np.r_[0., 0., meshGen.csz/2.]
src_b = meshGen.mesh.gridFz[sim3D.src.surface_electrode, :][0, :] - np.r_[0., 0., meshGen.csz/2.]

srcDC = DC.Src.Dipole([], src_a, src_b)
probDC = DC.Problem3D_CC(meshGen.mesh, sigmaMap=sim3D.physprops.wires.sigma, bc_type='Dirichlet', Solver=Pardiso)
surveyDC = DC.Survey([srcDC])

probDC.pair(surveyDC)
```

    Homogeneous Dirichlet is the natural BC for this CC discretization.

```python
%%time
fieldsDC = probDC.fields(sim3D.physprops.model)
# np.save('/'.join([simDir, 'fieldsDC.npy']), fieldsDC[:, 'phiSolution'])
# phi = np.load('/'.join([simDir, 'fieldsDC.npy']))
# probDC.sigma = sim3D.physprops.sigma
# fieldsDC = probDC.fieldsPair(cylmesh, surveyDC)
# fieldsDC[:,'phiSolution'] = phi
```

    CPU times: user 40.9 s, sys: 2.44 s, total: 43.3 s
    Wall time: 44.6 s

# Plot

```python
prob2D = sim2D.prob
survey2D = sim2D.survey
```

```python
srctdem = survey2D.srcList[0]
```

```python
# (mesh2D.mesh.faceDiv * utils.sdiag(prob2D.mesh.area) * srctdem._s_e).nonzero()
rhs = srctdem.getRHSdc(prob2D)
```

```python
inds = np.abs(rhs) > 1e-10
mesh2D.mesh.gridCC[inds]
```

    array([[  9.87500000e+02,   0.00000000e+00,  -3.75000000e+00],
           [  9.62500000e-02,   0.00000000e+00,  -1.25000000e+00]])

```python
rhs[inds]
```

    array([ 1., -1.])

```python
j_initial = srctdem.jInitial(prob2D)
```

```python
srctdem.getRHSdc(prob2D)[srctdem.getRHSdc(prob2D).nonzero()]
```

    array([  1.00000000e+00,  -1.00000000e+00,   2.22044605e-16,
            -2.22044605e-16,  -1.11022302e-16,   3.33066907e-16,
            -1.11022302e-16,  -2.22044605e-16,   1.11022302e-16,
            -2.22044605e-16,   1.11022302e-16,  -1.11022302e-16,
            -2.22044605e-16,   1.11022302e-16,  -3.33066907e-16,
             2.22044605e-16,  -1.11022302e-16,   1.11022302e-16,
             1.11022302e-16,   1.11022302e-16,  -1.11022302e-16,
            -1.11022302e-16,  -1.11022302e-16,  -1.11022302e-16,
             1.11022302e-16,  -1.11022302e-16,  -2.22044605e-16,
             1.11022302e-16,  -1.11022302e-16,  -2.22044605e-16,
             3.33066907e-16,  -2.22044605e-16,   1.11022302e-16,
            -1.11022302e-16,   2.22044605e-16,   2.22044605e-16,
            -2.22044605e-16,   1.11022302e-16,   1.11022302e-16,
            -1.11022302e-16,   2.22044605e-16,   2.22044605e-16,
            -2.22044605e-16,  -1.11022302e-16])

```python
import ipywidgets
```

```python
def plotCurrents(tind=0):
    jtdem = fields2D[:,'j',tind]
#     jtdem = j_initial
    jdc = fieldsDC[:, 'j']

    fig, ax = plt.subplots(1,2, figsize=(15, 6))

    for i, jplt in enumerate([jtdem, jdc]):
        casingSimulations.plotFace2D(
            mesh2D.mesh,
            jplt, real_or_imag='real', ax=ax[i], range_x=np.r_[-0.5, 0.5],
            range_y=np.r_[-1500., 0.], sample_grid=np.r_[1e-2, 10.],
            logScale=True, clim=[1e-9, 1e-1],
            mirror=True
        )
    ax[0].set_title('j at t={}s'.format(sim2D.prob.times[tind]))
    ax[1].set_title('j DC')
    plt.show()

ipywidgets.interact(plotCurrents, tind=ipywidgets.IntSlider(min=0, max=len(cp.timeSteps)-1, step=1), theta_ind=ipywidgets.IntSlider(min=0, max=cylMeshGen.mesh.nCy-1, step=1))
```

```python
casing_faces_x = (
    (sim2D.meshGenerator.mesh.gridFx[:, 0] >= cp.casing_a) &
    (sim2D.meshGenerator.mesh.gridFx[:, 0] <= cp.casing_b) &
    (sim2D.meshGenerator.mesh.gridFx[:, 2] <= cp.casing_z[1]) &
    (sim2D.meshGenerator.mesh.gridFx[:, 2] >= cp.casing_z[0])
)
casing_faces_z = (
    (sim2D.meshGenerator.mesh.gridFz[:, 0] >= cp.casing_a) &
    (sim2D.meshGenerator.mesh.gridFz[:, 0] <= cp.casing_b) &
    (sim2D.meshGenerator.mesh.gridFz[:, 2] <= cp.casing_z[1]) &
    (sim2D.meshGenerator.mesh.gridFz[:, 2] >= cp.casing_z[0])
)
```

```python
j = fields2D[:, 'j', :]
jA = utils.sdiag(sim2D.meshGenerator.mesh.area) * j

jACasing = utils.sdiag(
    np.hstack([casing_faces_x, casing_faces_z])
) * jA

ixCasing = []
izCasing = []

for ind in range(sim2D.prob.nT+1):
    jxCasing = jACasing[:sim2D.meshGenerator.mesh.nFx, ind].reshape(
        sim2D.meshGenerator.mesh.vnFx[0], sim2D.meshGenerator.mesh.vnFx[2], order='F'
    )
    jzCasing = jACasing[sim2D.meshGenerator.mesh.nFx:, ind].reshape(
        sim2D.meshGenerator.mesh.vnFz[0], sim2D.meshGenerator.mesh.vnFz[2], order='F'
    )

    ixCasing.append(jxCasing.sum(0))
    izCasing.append(jzCasing.sum(0))

ixCasing=np.vstack(ixCasing).T
izCasing=np.vstack(izCasing).T
```

```python
jxCasing.shape
```

    (130, 446)

```python
zcasing_indz = (
    (sim2D.meshGenerator.mesh.vectorNz >= cp.casing_z[0]) &
    (sim2D.meshGenerator.mesh.vectorNz <= cp.casing_z[1])
)
zcasing_z = sim2D.meshGenerator.mesh.vectorNz[zcasing_indz]

zcasing_indx = (
    (sim2D.meshGenerator.mesh.vectorCCz >= cp.casing_z[0]) &
    (sim2D.meshGenerator.mesh.vectorCCz <= cp.casing_z[1])
)
zcasing_x = sim2D.meshGenerator.mesh.vectorCCz[zcasing_indx]
```

```python
inds = np.arange(1, sim2D.prob.nT+1, step=10)
fig, ax = plt.subplots(2, 1, figsize=(15, 12))

color_idx = np.linspace(0, 0.8, len(inds))
for col_id, ind in zip(color_idx, inds):
    ax[0].semilogy(
        sim2D.meshGenerator.mesh.vectorCCz[zcasing_indx], -ixCasing[:, ind][zcasing_indx], '--', color=plt.cm.viridis(col_id)
    )
    ax[0].semilogy(
        sim2D.meshGenerator.mesh.vectorCCz[zcasing_indx], ixCasing[:, ind][zcasing_indx], '-', color=plt.cm.viridis(col_id),
        label='{:1.1e} s'.format(sim2D.prob.times[ind])
    )

    ax[1].semilogy(
        sim2D.meshGenerator.mesh.vectorCCz[zcasing_indz], -izCasing[:, ind][zcasing_indz], '--', color=plt.cm.viridis(col_id)
    )
    ax[1].semilogy(
        sim2D.meshGenerator.mesh.vectorCCz[zcasing_indz], izCasing[:, ind][zcasing_indz], '-', color=plt.cm.viridis(col_id),
        label='{:1.1e} s'.format(sim2D.prob.times[ind])
    )

for a in ax:
    a.grid('both', linestyle='-', linewidth=0.4, color=[0.8, 0.8, 0.8])
    a.legend(bbox_to_anchor=(1.05, 1), loc=2)
    a.set_ylim([1e-6, 1e0])
    a.set_xlim([1., -1000.])

# plt.plot(izCasing)
```

# Load up 3D results

```python
meshGen = casingSimulations.load_properties('/'.join([simDir, 'MeshParameters.json']))
cp = casingSimulations.load_properties('/'.join([simDir, 'CasingParameters.json']))
jfield = np.load('/'.join([simDir, 'fields.npy']))
src = casingSimulations.load_properties('/'.join([simDir, 'Source.json']), targetModule=casingSimulations.sources)
# simulation = casingSimulations.load_properties(
#     '/'.join([simDir, 'simulationParameters.json']),
#     targetClass=casingSimulations.run.SimulationFDEM
# )
```

    /Users/lindseyjh/git/python_symlinks/properties/base/base.py:411: RuntimeWarning: Unused properties during deserialization: hy, npadz, npadx, pfz, csz, pfx2, csx2, csx1, domain_x2, nca, ncb, pfx1
      ), RuntimeWarning)

```python
sim3D = casingSimulations.run.SimulationTDEM(cp=cp, meshGenerator=meshGen, src=src)
sim3D.prob.model = sim3D.physprops.model
```

```python
%%time
fields3D = sim3D.prob.fieldsPair(meshGen.mesh, sim3D.survey)
```

    CPU times: user 3min 17s, sys: 4.59 s, total: 3min 22s
    Wall time: 3min 26s

```python
fields3D[:,'jSolution'] = jfield
srcList = sim3D.survey.srcList
```

```python
cylmesh = meshGen.mesh
src = sim3D.src
```

```python
fields3D.aliasFields
```

    {'dhdt': ['jSolution', 'E', '_dhdt'], 'j': ['jSolution', 'F', '_j']}

# Plot the Results

```python
import ipywidgets
```

```python
plotopts = {
    'range_x':np.r_[0., 2000.],
    'range_y':np.r_[-2000., 10.],
    'sample_grid':np.r_[10., 10.],
    'logScale':True,
    'clim':[1e-9, 1e-5]
}

def plotCurrents(tind=0, theta_ind=0):
    jfull = fields3D[:,'j',tind]
    jtdem = casingSimulations.face3DthetaSlice(meshGen.mesh, jfull, theta_ind=theta_ind)

    j2D = fields2D[:, 'j', tind]
#     jtdem = j_initial
#     jdc = fieldsDC[:, 'j']

    fig, ax = plt.subplots(1,2, figsize=(15, 8))

    for i, jplt in enumerate([jtdem, j2D]):
        casingSimulations.plotFace2D(
            mesh2D,
            jplt, real_or_imag='real', ax=ax[i], range_x=np.r_[0, 1200],
            range_y=np.r_[-1500., 0.], sample_grid=np.r_[10., 10.],
            logScale=True, clim=[1e-9, 1e-4],
            mirror=False
        )
    ax[1].set_title('j 2D at t={}s'.format(sim2D.prob.times[tind]))
    ax[0].set_title('j 3D at t={}s, theta={:1.1f}'.format(sim2D.prob.times[tind], meshGen.mesh.vectorCCy[theta_ind]*180/np.pi))
    plt.show()

ipywidgets.interact(plotCurrents, tind=ipywidgets.IntSlider(min=0, max=len(cp.timeSteps)-1, step=1), theta_ind=ipywidgets.IntSlider(min=0, max=meshGen.mesh.nCy-1, step=1))

```

```python
plotopts = {
    'range_x':np.r_[0., 2000.],
    'range_y':np.r_[-2000., 10.],
    'sample_grid':np.r_[10., 10.],
    'logScale':True,
    'clim':[1e-9, 1e-5]
}

def plotCurrentswDC(tind=0, theta_ind=0):
    jfull = fields3D[:,'j',tind]
    jtdem = casingSimulations.face3DthetaSlice(meshGen.mesh, jfull, theta_ind=theta_ind)

    jDC = fieldsDC[:, 'j']
    jDC = casingSimulations.face3DthetaSlice(meshGen.mesh, jDC, theta_ind=theta_ind)

    jdiff = jtdem - jDC
#     jtdem = j_initial
#     jdc = fieldsDC[:, 'j']

    fig, ax = plt.subplots(1,3, figsize=(15, 6))

    for i, jplt in enumerate([jtdem, jDC, jdiff]):
        casingSimulations.plotFace2D(
            mesh2D,
            jplt, real_or_imag='real', ax=ax[i], range_x=np.r_[0, 1200],
            range_y=np.r_[-1500., 0.], sample_grid=np.r_[10., 10.],
            logScale=True, clim=[1e-9, 1e-4],
            mirror=False
        )

    ax[2].set_title('j diff at t={}s'.format(sim2D.prob.times[tind]))
    ax[1].set_title('j DC at t={}s'.format(sim2D.prob.times[tind]))
    ax[0].set_title('j 3D at t={}s, theta={:1.1f}'.format(sim2D.prob.times[tind], meshGen.mesh.vectorCCy[theta_ind]*180/np.pi))

    plt.tight_layout()
    plt.show()

ipywidgets.interact(plotCurrentswDC, tind=ipywidgets.IntSlider(min=0, max=len(cp.timeSteps)-1, step=1), theta_ind=ipywidgets.IntSlider(min=0, max=meshGen.mesh.nCy-1, step=1))

```

```python
fig, ax = plt.subplots(1, 2, figsize=(10, 4))
ax = utils.mkvc(ax)

casingSimulations.view.plotLinesFx(
    cylmesh,
    sim3D.survey.srcList[2:],
    fields3D=fields3D,
    fieldsDC=fieldsDC,
    fieldType='e',
    pltType='loglog',
    ax=ax,
    theta_ind=0,
    xlim=[10., 10000.],
    zloc=0.
)
```

```python
cp.serialize()
```

    {u'__class__': 'CasingParameters',
     'casing_d': 0.2,
     'casing_l': 1000.0,
     'casing_t': 0.01,
     'casing_top': 0.0,
     'freqs': [0.0001, 0.001, 0.01, 0.1, 1.0],
     'layer_z': [-1000.0, -900.0],
     'mur_casing': 100.0,
     'sigma_air': 1e-06,
     'sigma_back': 0.1,
     'sigma_casing': 5500000.0,
     'sigma_inside': 1.0,
     'sigma_layer': 0.1,
     'src_a': [0.0, 3.141592653589793, 0.0],
     'src_b': [1000.0, 3.141592653589793, 0.0],
     'version': u'0.0.1'}

```python
import os
```

```python
os.path.expanduser('~')
```

    '/Users/lindseyjh'

```python
os.path.abspath(os.path.expanduser('.'))
```

    '/Users/lindseyjh/git/casingResearch/notebooks/casing3D'

```python
downloadpath = [os.path.sep.join([path, f]) for f in filenames]
```

```python

```


```python
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
from scipy.optimize import fsolve
from scipy.integrate import quad
from scipy.special import eval_laguerre
import itertools
import copy
from image_matrix_helper import compute_master_list, imshow_list, rgb_map, color_to_rgb, list_to_matrix
import random
import time

nb_start = time.time()
```

## Simulation for General Case of Ligand-Receptor Binding

In this notebook, we simulate a thermal system of particles of various colors binding onto a grid. We have $R$ different types of particles and particle of type $j$ has $n_j$ copies in the system. Particles can exist both on and off the grid and particles of type $j$ have a binding affinity of $\gamma_j$ to the grid. Each particle type also has a collection of "correct" locations on the grid. A particle of type $j$ binds to this correct location with an additional optimal binding affinity factor of $\delta_j$ (i.e., its net affinity to such a site is $\gamma_j \delta_j$). Here we want to use simulations of this system to affirm analytical calculations of the average number of bound particles and the average number of correctly bound particles as functions of temperature.

### Numerical representations of analytical work

#### Equations of Large $N$ approximation

In the large $N$ limit, the order parameters for the system can be approximated as

\begin{equation}
\langle k \rangle =   \sum_{j=1}^r \frac{n_j }{\bar{z} \gamma_j +1} \left(\bar{z} \gamma_j +  \frac{\displaystyle L_{n_j-1} \left( \bar{\phi}_{j}\right)}{\displaystyle  L_{n_j} \left( \bar{\phi}_{j} \right)}\right) \qquad \langle m \rangle  =   \sum_{j=1}^r \frac{n_j \delta_j}{\delta_j-1} \frac{\displaystyle L_{n_j-1} \left( \bar{\phi}_{j}\right)}{\displaystyle  L_{n_j} \left( \bar{\phi}_{j} \right)},
\end{equation}
where $\bar{z}$ and $\bar{x}$ are defined as
\begin{equation}
\bar{z} = \sum_{j=1}^{R} \frac{n_j}{\bar{z} \gamma_j +1} \left(1- \frac{\displaystyle L_{n_j-1} \left( \bar{\phi}_{j}\right)}{\displaystyle  L_{n_j} \left( \bar{\phi}_{j} \right)}\right), \qquad \bar{x} = \sum_{j=1}^{R} n_j\left(1- \frac{\displaystyle L_{n_j-1} \left( \bar{\phi}_{j}\right)}{\displaystyle  L_{n_j} \left( \bar{\phi}_{j} \right)}\right)
\end{equation}
with
\begin{equation}
\bar{\phi}_{j}\equiv \frac{\bar{x}}{1-\delta_j}\left(1+ \frac{1}{\bar{z} \gamma_j}\right).
\label{eq:phi_def}
\end{equation}
and $L_n(x)$ the $n$th Laguerre polynomial.

For these simulations we will take $$\gamma_j = (\beta E_V)^{3/2} e^{-\beta E_j}, \qquad \delta_j = e^{\beta \Delta_j}$$ where $E_V$ is a volumetric Boltzmann factor associated with free particles (e.g., for a point-particle $E_V \equiv h^2/2\pi mV^{2/3}$), and $E_j$ is the binding energy for particles to the grid. We also take where $\Delta_j$ is the binding energy advantage for particles binding to their correct locations in the grid.

#### Parameter function definitions

```python
# helper function definitions
gamma_func = lambda E0, Ev, T: 4*np.sqrt(2)*np.exp(E0/T)*(Ev/T)**(3/2)
delta_func = lambda Del, T: np.exp(Del/T)
phi_func = lambda x, z, gamma, delta: x*(1+ 1/(z*gamma))/(1-delta)
```

#### Equilibrium equations

```python
def constr_func(X, T, E0s, Dels, Evs, Ns):

    """
    Equations constraint equations that determine zbar and xbar
    """

    x = X[0]
    z = X[1]

    F = np.ones(2)

    R = len(Ns)
    gammas_ = gamma_func(E0s,Evs, T)
    deltas_ = delta_func(Dels, T)
    phis_ = phi_func(x, z, gammas_, deltas_)

    F[0] = z- np.sum([Ns[j]/(z*gammas_[j]+1)*(1-eval_laguerre(Ns[j]-1, phis_[j])/eval_laguerre(Ns[j], phis_[j])) for j in range(R)])
    F[1] = x- np.sum([Ns[j]*(1-eval_laguerre(Ns[j]-1, phis_[j])/eval_laguerre(Ns[j], phis_[j]) ) for j in range(R)])

    return F

def m_avg(T, E0s, Dels, Evs, Ns):

    """
    Function that computes m_avg
    """

    x, z =  fsolve(constr_func, x0 = (50,500), args = (T, E0s, Dels, Evs, Ns))

    R = len(Ns)
    gammas_ = gamma_func(E0s,Evs, T)
    deltas_ = delta_func(Dels, T)
    phis_ = phi_func(x, z, gammas_, deltas_)

    return np.sum([Ns[j]*deltas_[j]/(deltas_[j]-1)*eval_laguerre(Ns[j]-1, phis_[j])/eval_laguerre(Ns[j], phis_[j]) for j in range(R)] )

def k_avg( T, E0s, Dels, Evs, Ns):

    """
    Function that computes k_avg
    """

    x, z = fsolve(constr_func, x0 = (50,500), args = (T, E0s, Dels, Evs, Ns))

    R = len(Ns)
    gammas_ = gamma_func(E0s,Evs, T)
    deltas_ = delta_func(Dels, T)
    phis_ = phi_func(x, z, gammas_, deltas_)

    return np.sum([Ns[j]/(z*gammas_[j]+1)*(z*gammas_[j] + eval_laguerre(Ns[j]-1, phis_[j])/eval_laguerre(Ns[j], phis_[j])) for j in range(R)])
```

#### Important temperatures

The temperature $k_BT_{\text{crit}}$ at which the system settles into its completely correct configuration is defined by

\begin{equation}
1 = \sum_{j=1}^{R}n_j e^{-\beta_{\text{crit}} \Delta_j}\left( 1+ (\beta_{\text{crit}} E_V)^{-3/2} e^{-\beta_{\text{crit}} E_j}\right) + O(e^{-2\beta_{\text{crit}} \Delta_j}).
\label{eq:master_therm_subs}
\end{equation}

```python
# general thermal constraint
def master_them_constr(T, E0s, Dels, Evs, Ns):

    F =  1-np.sum(Ns*delta_func(Dels, T)**(-1)*(1+gamma_func(E0s, Evs, T)**(-1)))

    return F

# critical temperature
kBTcrit_master = lambda E0s, Dels, Evs, Ns: fsolve(master_them_constr, x0 = 100.5, args = (E0s, Dels, Evs, Ns))[0]
```

#### Example analytical plot

```python
# temperature vals
Tvals = np.linspace(0.1, 3.0, 50)

# parameters for the integral
np.random.seed(42)

R = 50
E0_bar, sigma_E = 10.0, 2.0
Del_bar, sigma_D = 3.0, 1.0
E0s = np.random.randn(R)*sigma_E+E0_bar
Dels = np.random.randn(R)*sigma_D+Del_bar
Nelems = np.random.randint(1,10,R)
Evs = np.ones(R)*0.001

# computing analytical values of k and m
avg_k_approx_vals = [k_avg(T, E0s, Dels, Evs, Nelems) for T in Tvals]
avg_m_approx_vals = [m_avg(T, E0s, Dels, Evs, Nelems) for T in Tvals]
```

    <ipython-input-2-a582446d2d66>:4: RuntimeWarning: divide by zero encountered in true_divide
      phi_func = lambda x, z, gamma, delta: x*(1+ 1/(z*gamma))/(1-delta)
    <ipython-input-3-08aba1dbaf99>:17: RuntimeWarning: invalid value encountered in double_scalars
      F[0] = z- np.sum([Ns[j]/(z*gammas_[j]+1)*(1-eval_laguerre(Ns[j]-1, phis_[j])/eval_laguerre(Ns[j], phis_[j])) for j in range(R)])
    <ipython-input-3-08aba1dbaf99>:18: RuntimeWarning: invalid value encountered in double_scalars
      F[1] = x- np.sum([Ns[j]*(1-eval_laguerre(Ns[j]-1, phis_[j])/eval_laguerre(Ns[j], phis_[j]) ) for j in range(R)])
    <ipython-input-2-a582446d2d66>:4: RuntimeWarning: invalid value encountered in multiply
      phi_func = lambda x, z, gamma, delta: x*(1+ 1/(z*gamma))/(1-delta)
    /Users/mobolajiwilliams/opt/miniconda3/lib/python3.8/site-packages/scipy/optimize/minpack.py:175: RuntimeWarning: The iteration is not making good progress, as measured by the
      improvement from the last ten iterations.
      warnings.warn(msg, RuntimeWarning)

```python
## plotting order parameters
ax = plt.subplot(111)
ax.plot(Tvals, avg_k_approx_vals/np.sum(Nelems), label = r'$\langle k \rangle$ (large $N$)')
ax.plot(Tvals, avg_m_approx_vals/np.sum(Nelems), label = r'$\langle m \rangle$ (large $N$)')
ax.set_xlabel(r'$k_BT$', fontsize = 18, labelpad = 10.5)
ax.grid(alpha = 0.5)
ax.axvline(x = kBTcrit_master(E0s, Dels, Evs, Nelems), color = 'g', linestyle = '--')

# Hide the right and top spines
ax.spines['right'].set_visible(False)
ax.spines['top'].set_visible(False)

# Only show ticks on the left and bottom spines
ax.yaxis.set_ticks_position('left')
ax.xaxis.set_ticks_position('bottom')
```

### Metropolis Hastings simulation code

#### Microstate transitions

```python
## dissociation operator
def trans_dissoc(free_objs, bound_objs):

        # indices of non-empty
        indxs = [i for i, x in enumerate(bound_objs) if x != "-"]

        # random choice for bound object
        random_indx = random.choice(indxs)

        ## new state vector
        free_objs_new = copy.deepcopy(free_objs)
        bound_objs_new = copy.deepcopy(bound_objs)

        # putting empty slot
        bound_objs_new[random_indx] = '-'
        # appending previously bound object to free objects
        free_objs_new.append(bound_objs[random_indx])

        return free_objs_new, bound_objs_new

## association operator
def trans_assoc(free_objs, bound_objs):

        # random element to associate
        elem = random.choice(free_objs)

        # indices of empty spaces
        indxs = [i for i, x in enumerate(bound_objs) if x == "-"]

        # random choice for empty space
        random_indx = random.choice(indxs)

        ## new state vector
        free_objs_new = copy.deepcopy(free_objs)
        bound_objs_new = copy.deepcopy(bound_objs)

        ## state
        free_objs_new.remove(elem)
        bound_objs_new[random_indx] = elem

        return free_objs_new, bound_objs_new

## permutation operator
def trans_perm(free_objs, bound_objs):

    Ncomp = len(bound_objs)
    i1 = int(random.choice(range(Ncomp)))
    i2 = int(random.choice(range(Ncomp)))

    ## new omega vector
    bound_objs_new = copy.deepcopy(bound_objs)
    bound_objs_new[i2] = bound_objs[i1]
    bound_objs_new[i1] = bound_objs[i2]

    return free_objs, bound_objs_new
```

#### Logarithm of Botlzmann factor

The logarithm of the Botlzmann factor for a microstate (i.e., the temperature normalized negative energy of the microstate) is defined as

\begin{equation}
\beta E(\boldsymbol{k}, \boldsymbol{m}) = \sum_{i=1}^R(m_i \ln \delta_i + k_i \ln \gamma_i).
\label{eq:sim_en}
\end{equation}

```python
def log_boltz(free_objs, bound_objs, mstr_vec, deltas, gammas, name_key):

    elem_set = list(set(mstr_vec))
    count_dict = dict()
    for elem in elem_set:
        count_dict[elem] = bound_objs.count(elem)

    bind_log_factor = 0
    for elem in elem_set:
        key = name_key[elem]
        bind_log_factor += count_dict[elem]*np.log(gammas[key])

    corr_log_factor = 0
    for j in range(len(bound_objs)):
        if bound_objs[j] == mstr_vec[j]:
            elem = bound_objs[j]
            key = name_key[elem]
            corr_log_factor+=np.log(deltas[key])

    return bind_log_factor+corr_log_factor
```

#### Function to count the number of correctly bound particles

```python
def m_calc(bound_objs, mstr_vec):

    num = 0
    for k in range(len(mstr_vec)):
        if mstr_vec[k] == bound_objs[k]:
            num += 1
    return num
```

#### Checking logarithm of Boltzmann factor definition

```python
# defining name key
name_key0 = dict()
key_list = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', ]
for j in range(len(key_list)):
    name_key0[key_list[j]] = j

# random energies
np.random.seed(2)
q1 = np.random.rand(10)
q2 = np.random.rand(10)

# sample master list
sample_master = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', ]
bound_init_0 = ['-', '-', '-', '-', '-', '-', 'G', 'H', 'I', 'J', ]
free_init_0 = ['A', 'B', 'C', 'D', 'E', 'F' ]

print('Energy for original ordering:', log_boltz( free_init_0,bound_init_0, sample_master, deltas = np.exp(-q1), gammas = np.exp(-q2), name_key =  name_key0))
e1 = -np.sum([q1[k] for k in range(len(sample_master)) if sample_master[k] == bound_init_0[k]])
e2 = -np.sum([q2[k] for k in range(len(sample_master)) if sample_master[k] in bound_init_0])
print('Checking  energy value:', e1+e2)
print('Number of correctly placed elements:', m_calc(bound_init_0, sample_master))
print('Number of bound elements:', np.sum([1 for elem in bound_init_0 if elem!='-']))
print('-----')
random.seed(1)
free_init_0, perm_bound = trans_perm(free_init_0, bound_init_0)
free_init_new, bound_init_new = trans_assoc(free_init_0, perm_bound)
print('Energy after permutation and associaation values:', log_boltz(free_init_new, bound_init_new, sample_master, deltas = np.exp(-q1), gammas = np.exp(-q2), name_key =  name_key0))
e1 = -np.sum([q1[k] for k in range(len(sample_master)) if sample_master[k] == bound_init_new[k]])
e2 = -np.sum([q2[k] for k in range(len(sample_master)) if sample_master[k] in bound_init_new])
print('Checking  energy value:', e1+e2)
print('Number of correctly placed elements:', m_calc(bound_init_new, sample_master))
print('Number of bound elements:', np.sum([1 for elem in bound_init_new if elem!='-']))
```

    Energy for original ordering: -3.664820641553842
    Checking  energy value: -3.664820641553841
    Number of correctly placed elements: 4
    Number of bound elements: 4
    -----
    Energy after permutation and associaation values: -4.01912719922027
    Checking  energy value: -4.01912719922027
    Number of correctly placed elements: 3
    Number of bound elements: 5

#### Metropolis Hastings algorithm

```python
### Metropolis Monte Carlo Algorithm

## loads uniform random sampling
runif = np.random.rand

def met_assembly_grid(Niter, free_objs, bound_objs, mstr_vec, deltas, gammas, name_key):
    '''
    #################################################################
    # function to sample using Metropolis
    #
    # n_iter:  number of iterations
    # initial_state: initial state for the start position for our chain
    # gamma: energy cost for incorrect component
    # temp: temperature
    ##################################################################
    '''

    # Initialize state values
    free_objs_vals = [0]*(Niter+1)
    bound_objs_vals = [0]*(Niter+1)

    # Set initial values
    free_objs_vals[0] = free_objs[:]
    bound_objs_vals[0] = bound_objs[:]

    # Initialize acceptance counts
    # We can use this to tune our number of steps
    accepted = 0

    # debugging code
    debug_assoc, debug_dissoc, debug_perm = 0, 0, 0

    for i in range(Niter):

        # get current monomer and dimer states
        current_free_objs = copy.deepcopy(free_objs_vals[i])
        current_bound_objs = copy.deepcopy(bound_objs_vals[i])

        N_free = len(current_free_objs)
        N_bound = len(current_bound_objs)-len(current_free_objs)

        u_trans = runif()

        if u_trans < 1/3: #first type of transition; monomer association

            if N_free < 1:

                log_alpha = np.log(1e-15)

            else:
                # proposed new monomer and dimer states
                new_free_objs, new_bound_objs = trans_assoc(current_free_objs, current_bound_objs)

                # transition elements
                log_init = log_boltz(current_free_objs, current_bound_objs, mstr_vec, deltas, gammas, name_key)
                log_final = log_boltz(new_free_objs, new_bound_objs, mstr_vec, deltas, gammas, name_key)

                # weight
                num = N_free*N_free
                den = N_bound+1

                # Log-acceptance rate
                log_alpha = log_final-log_init+np.log(num/den)

        elif 1/3 <= u_trans < 2/3: #second type of transition; bound monomer dissociation

            if N_bound <1:

                log_alpha = np.log(1e-15)

            else:

                # proposed new monomer and dimer states
                new_free_objs, new_bound_objs = trans_dissoc(current_free_objs, current_bound_objs)

                # transition elements
                log_init = log_boltz(current_free_objs, current_bound_objs, mstr_vec, deltas, gammas, name_key)
                log_final = log_boltz(new_free_objs, new_bound_objs, mstr_vec, deltas, gammas, name_key)

                # weight
                num = N_bound
                den = (N_free+1)*(N_free+1)

                # Log-acceptance rate
                log_alpha = log_final-log_init+np.log(num/den)

        elif 2/3 <= u_trans: #third type of transition; switching bounded elements

            if N_bound <2:

                log_alpha = np.log(1e-15)

            else:

                # proposed new monomer and dimer states
                new_free_objs, new_bound_objs = trans_perm(current_free_objs, current_bound_objs)

                # transition elements
                log_init = log_boltz(current_free_objs, current_bound_objs, mstr_vec, deltas, gammas, name_key)
                log_final = log_boltz(new_free_objs, new_bound_objs, mstr_vec, deltas, gammas, name_key)

                # Log-acceptance rate
                log_alpha = log_final-log_init

        # Sample a uniform random variate
        u = runif()

        # Test proposed value
        if np.log(u) < log_alpha:
            # Accept
            free_objs_vals[i+1] = new_free_objs
            bound_objs_vals[i+1] = new_bound_objs

            #log_current_prob = log_proposed_prob
            accepted += 1
        else:
            # Stay put
            free_objs_vals[i+1] = free_objs_vals[i]
            bound_objs_vals[i+1] = bound_objs_vals[i]

    # return our samples and the number of accepted steps
    return free_objs_vals, bound_objs_vals, accepted
```

#### Computing microstate averages from simiulations

```python
def avg_k(bound_objs_vals, Nmc):

    """
    Microstate average of number of bound objects
    We only consider microstates near the end of theh chain to ensure
    that the system has equilibrated
    """

    length = int(Nmc/50)

    ls = [0]*length
    ls = np.array(ls)
    for k in range(length):
        ls[k] = len(bound_objs_vals[Nmc-length+k]) - bound_objs_vals[Nmc-length+k].count('-')

    return(np.mean(ls))

# average number of correctly bound objects
def avg_m(bound_objs_vals, mstr_vec, Nmc):

    """
    Microstate average of number of correctly bound objects
    We only consider microstates near the end of theh chain to ensure
    that the system has equilibrated
    """

    length = int(Nmc/50)

    ls = [0]*length
    ls = np.array(ls)
    for k in range(length):
        ls[k] =  np.sum([1 for j in range(len(mstr_vec)) if bound_objs_vals[Nmc-length+k][j]==mstr_vec[j]])

    return(np.mean(ls))
```

#### Image grid for completely correct configuration

```python
# defining master_list
master_list =compute_master_list()
# testing plot
imshow_list(master_list, title = 'Completely Correct Configuration');
# defining Nelems
Nelems = np.zeros(8)
key_list = list(rgb_map.keys())[:-1]
name_key_ = dict()
for j in range(len(key_list)):
    name_key_[key_list[j]] = j
    Nelems[j] = master_list.count(key_list[j])
```

```python
# displaying copy-number counts of the various elements
Nelems
```

    array([ 9.,  9., 10.,  5.,  7.,  6.,  3., 51.])

#### Simulating system

```python
## Generate lf for each temperature from .03 to 2.0 in npoints steps
t0 = time.time()

# number of steps for MC algortihm
Nmc = 30000

# parameter definitions
R = 8
np.random.seed(24)
Del_bar, sigma_D = 3.0, 1.0
Dels = np.random.randn(R)*sigma_D+Del_bar
E0_bar, sigma_E = 14.0, 2.0
E0s = np.random.randn(R)*sigma_E+E0_bar
Evs = np.ones(R)*0.001

# initial monomer and dimer states;
# system in microstate of all correct dimers
random.seed(0)
free_objs_0 = []
bound_objs_0 = random.sample(master_list, len(master_list))
mstr_vec = copy.deepcopy(master_list)

# temperature limits
Tmin = .05
Tmax = 3.0

npoints = 15 #number of temperature values
navg = 5 # number of times we run simulation at each temperature; 50 in paper
temp_vals = np.linspace(Tmin, Tmax, npoints).tolist()

# list of dimer values
sim_k_vals = [0]*npoints
# list of correct dimer values
sim_m_vals = [0]*npoints
# accepted list
accepted_list = [0]*npoints
# saved list for plotting
saved_list = dict()

for k in range(npoints):

    fin_k_vals = [0]*navg
    fin_m_vals = [0]*navg
    fin_accepted = [0]*navg

    for j in range(navg):

        # make copy of initial monomer and dimer states
        free_objs_copy = copy.deepcopy(free_objs_0)
        bound_objs_copy = copy.deepcopy(bound_objs_0)

        # defining helper functions
        gammas_ = gamma_func(E0s, Evs, temp_vals[k])
        deltas_ = delta_func(Dels, temp_vals[k])

        # metroplois generator
        free_list, bound_list, accepted = met_assembly_grid(Nmc,
                                                            free_objs_copy,
                                                            bound_objs_copy,
                                                            mstr_vec,
                                                            deltas_,
                                                            gammas_,
                                                            name_key_)

        # averaging final states to compute observables
        fin_k_vals[j] = avg_k(bound_list, Nmc)
        fin_m_vals[j] = avg_m(bound_list, mstr_vec, Nmc)
        fin_accepted[j] = accepted

    # saving every 5 temperatures
    if (k+1)%5 ==0 or k ==0:
        saved_list[k] = ['white' if x=='-' else x for x in bound_list[-1]]

    # averaging over computed equilibrium averages
    sim_k_vals[k] = np.mean(np.array(fin_k_vals))
    sim_m_vals[k] = np.mean(np.array(fin_m_vals))
    accepted_list[k] = np.mean(np.array(fin_accepted))

    t_prelim = time.time()
    print("Temperature Run:",str(k+1),"; Current Time:", round(t_prelim-t0,2),"secs")

t1 = time.time()
print("Total Simulation Run Time:",t1-t0,"secs")
```

    Temperature Run: 1 ; Current Time: 62.85 secs
    Temperature Run: 2 ; Current Time: 125.33 secs
    Temperature Run: 3 ; Current Time: 184.85 secs
    Temperature Run: 4 ; Current Time: 236.61 secs
    Temperature Run: 5 ; Current Time: 283.27 secs
    Temperature Run: 6 ; Current Time: 341.44 secs
    Temperature Run: 7 ; Current Time: 406.55 secs
    Temperature Run: 8 ; Current Time: 471.81 secs
    Temperature Run: 9 ; Current Time: 529.68 secs
    Temperature Run: 10 ; Current Time: 583.34 secs
    Temperature Run: 11 ; Current Time: 633.34 secs
    Temperature Run: 12 ; Current Time: 682.38 secs
    Temperature Run: 13 ; Current Time: 730.92 secs
    Temperature Run: 14 ; Current Time: 780.55 secs
    Temperature Run: 15 ; Current Time: 831.61 secs
    Total Simulation Run Time: 831.6078369617462 secs

#### Simulated image grid at various temperatures

```python
# figure parameters
rows, cols, idx = 2, 2, 0
fig, axes = plt.subplots(nrows=rows, ncols=cols, figsize=(9,9))

# list of keys for saved snapshots of image
img_key_list = list(saved_list.keys())

for i in range(rows):
    for j in range(cols):
        if idx < 4:
            axes[i, j].imshow(color_to_rgb(list_to_matrix(saved_list[img_key_list[idx]])))
            ax = plt.gca()

            # making ticks invisible
            axes[i, j].set_xticks([])
            axes[i, j].set_yticks([])

            # Minor ticks
            axes[i, j].set_xticks(np.arange(-0.5, 11, 1), minor=True)
            axes[i, j].set_yticks(np.arange(-0.5, 10, 1), minor=True)
            axes[i, j].tick_params(axis='y', colors='red')

            # labeling images
            itimes = 'i'*(1+idx) if idx<3 else 'iv'

            # Gridlines based on minor ticks
            axes[i, j].grid(which='minor', color='w', linestyle='-', linewidth=3)
            axes[i, j].set_title(fr'({itimes})  $k_BT = {round(temp_vals[img_key_list[idx]],2)}$', fontsize = 18, y = -.2)

            # making spines invisible
            axes[i, j].spines['right'].set_visible(False)
            axes[i, j].spines['top'].set_visible(False)
            axes[i, j].spines['left'].set_visible(False)
            axes[i, j].spines['bottom'].set_visible(False)

            idx +=1

# plt.savefig('general_grid_assembly_grid_plots.png', bbox_inches='tight', format = 'png')
plt.show()
```

#### Comparing analytical and simulation results

```python
plt.figure(figsize = (7,5))
ax = plt.subplot(111)

# simulation results
plt.plot(temp_vals,np.array(sim_k_vals)/np.sum(Nelems),
        label = r'Sim. $\langle k \rangle$/N',
        markersize = 7.5,
       marker = 'D',
       linestyle = '')
plt.plot(temp_vals,np.array(sim_m_vals)/np.sum(Nelems),
        label = r'Sim. $\langle m \rangle$/N',
        markersize = 7.5,
       marker = 's',
       linestyle = '')

# large N analytical results
k_avg_approx_vals = [k_avg(T, E0s, Dels, Evs, Nelems)/np.sum(Nelems) for T in Tvals]
m_avg_approx_vals = [m_avg(T, E0s, Dels, Evs, Nelems)/np.sum(Nelems) for T in Tvals]
plt.plot(Tvals, k_avg_approx_vals, label = r'Large $N$ $\langle k \rangle$/N', linestyle= '--', linewidth = 3.0)
plt.plot(Tvals, m_avg_approx_vals, label = r'Large $N$ $\langle m \rangle$/N', linewidth = 2.0 )
ax.axvline(x = kBTcrit_master(E0s, Dels, Evs, Nelems), color = 'k', linestyle = '-.', linewidth = 2)

plt.legend(loc = 'best', fontsize = 12)
# plot formatting
ax.set_xlabel(r'$k_B T$', fontsize = 18)
plt.xlim([-0.01,3.2])
plt.ylim([0,1.1])
plt.grid(alpha = 0.45)

# Hide the right and top spines
ax.spines['right'].set_visible(False)
ax.spines['top'].set_visible(False)

# Only show ticks on the left and bottom spines
ax.yaxis.set_ticks_position('left')
ax.xaxis.set_ticks_position('bottom')

# increase label size
ax.tick_params(axis='both', which='major', labelsize=12)
ax.tick_params(axis='both', which='minor', labelsize=12)
ax.text(kBTcrit_master(E0s, Dels, Evs, Nelems)-.2, 0.25, r'$k_BT_{crit}$', color='black', fontsize = 14.5,
        bbox=dict(facecolor='white', edgecolor='none', pad=5.0))

for i in range(4):
    ax.text(temp_vals[img_key_list[i]], sim_k_vals[img_key_list[i]]/np.sum(Nelems)+.05,'('+'i'*(1+i)+')' if i<3 else '(iv)', fontsize = 14 )

# plt.savefig(f'general_grid_assembly.png', bbox_inches='tight', format = 'png')
```

```python
print('Total Notebook Runtime: %.3f mins' % ((time.time()-nb_start)/60))
```

    Total Notebook Runtime: 13.918 mins


#  École des Ponts ParisTech
##  SPH pour l'hydraulique
###  Projet SPH GMM 2021
####  Parois mobiles
 Auteurs : Ruben Persicot & Yohan Lanier

 Encadrants : Rémi Carmigniani et Damien Violeau

## 1. Ecoulement de couette plan.

### 1.1 Etude analytique

$\textbf{Schéma du problème étudié :}$

$\textbf{Hypothèses :}$
 - (H1) La paroi supérieure est mobile, la paroi inférieure est immobile.
 - (H2) L'écoulement est incompressible.
 - (H3) L'écoulement est permanent.
 - (H4) Le gradient de pression est nul selon x. Autrement dit, la paroi supérieure est le seul moteur de l'écoulement

On cherche $\underline u$ de la forme $\underline u(x,y)= u(x,y)\underline{e_x}$.

$\textbf{Conditions aux limites :}$
  - en $y=+\frac{e}{2}, \underline{u}= U\underline{e_x}$  d'après (H1)
  - en $y=-\frac{e}{2}, \underline{u}= 0$                d'après (H1)

$\textbf{Résolution : }$
D'après (H2) on montre que $\underline u= u(y)\underline{e_x}$

On résout l'équation de Navier-Stockes pour l'écoulement considéré :

$\begin{array}{rcl}
\rho(\frac{\partial \underline{u}}{\partial t}+ \underline{u}\cdot\underline{\underline{grad}}(\underline{u}))&=&-\underline{grad}(p)+\rho \underline{g} + \mu \underline{\Delta} \underline{u} \\
0&=&-\underline{grad}(p)+\rho \underline{g} + \mu \underline{\Delta} \underline{u} \ \  \textrm{d'après (H2) et (H3)}
\end{array}$

On projette cette équation sur les axes $x$ et $y$ :
$ \left\{
    \begin{array}{ll}
        \mu\frac{\partial^2 u}{\partial x^2} = \frac{\partial p}{\partial x} = 0 \ \ \textrm{d'après (H4)}\\
        -\rho g = \frac{\partial p}{\partial y}\\
    \end{array}
\right.
$

Ainsi on montre que le profil de vitesse est linéaire selon $y$ :
$\begin{equation}
        u(y)= U(\frac{y}{e}+\frac{1}{2})
    \end{equation}$

Par ailleurs, le profil de pression est aussi linéaire selon $y$ :
$\begin{equation}
        p(y)= p_0 - \rho g y
    \end{equation}$

### 1.2 Etude numérique

#### Import des fichiers et des modules utiles pour programmer un code SPH.

```python
%load_ext autoreload
%autoreload 2
import numpy as np
from sys import exit
import os.path
from os import path
import csv
import time
import matplotlib
import matplotlib.pyplot as plt
#matplotlib.rcParams['text.usetex'] = True
from src.spyh import *
from src.sphvar import *
from src.plotParticles import *
from src.state import *
from src.contrib import *
from src.analytical_solutions import *
import numba
```

#### Définition des grandeurs du problèmes et du fluide

Les paramètres ci dessous correspondent à un fluide très visqueux puisque la viscosité dynamique est égale à 500 Pa/s.

On note que la gravité n'est pas prise en compte dans notre simulation. Celle-ci n'a aucune influence sur le profil de vitesse analytique. Néanmoins, elle joue un rôle déterminant dans la simulation numérique. Celui-ci sera détaillé plus précisément dans la suite du projet.

```python
#COUETTE PARAMETERS
e = 0.5 #Half width (m)
U = 1 # Upper bound velocity (m/s)
#FLUID PARAMETERS
Re = 1 # Reynolds number
rhoF = 1000
mu = rhoF*U*e/Re #Dynamic Vicosity (Pa.s)
grav = np.array([0.0,0.0])  #Gravity (m.s-2)
c0 = 10*U  #(m/s)
gamma = 7
B = rhoF*c0**2/gamma
#DENSITY & SHEPARD THRESHOLDS :
shepardMin = 10**(-6)
rhoMin = 0.5*rhoF
rhoMax = 1.5*rhoF
```

#### Définition des propriétés des particules

```python
#PARTICLE & SPACES PARAMETERS :
N = 4
M = 8
dr = 2*e/(M*N) #Particle radius
h = smthfc*dr
m=dr*dr*rhoF
lspace = 2*h
```

#### Création du domaine de calcul

Pour notre projet nous avons crée un nouveau type de particules dont le FLAG est MOBILEBOUND. Celles-ci permettent de modéliser le mouvement d'une paroi mobile dont on impose le mouvement.

Ce nouveau type de particule est défini dans [src/sphvar.py](src/sphvar.py)

Les murs seront composés d'une couche de *nBound =4* particules *fictives*. Ce nombre est suffisant pour éviter la pénétration du mur.

```python
#COMPUTATION DOMAIN
xOrigin = 0
yOrigin = -e-nBound*dr
xSize = 10*lspace
ySize = 2*e+2*nBound*dr
xMax = xOrigin+xSize
yMax = yOrigin+ySize
#INIT PART
part = init_particles()
part = addBox(part,[0,-e,xSize,2*e],FLUID,dr,rhoF)                  #Fluid domain
part = addBox(part,[0,-e-nBound*dr,xSize,nBound*dr],BOUND,dr,rhoF)  #Bound domain
part = addBox(part,[0,e,xSize,nBound*dr],MOBILEBOUND,dr,rhoF)       #Moving bound domain
#PERIODICITY VECTOR
vecPer = np.array([xSize,0])
posSpace,neibSpace,partSpace,listNeibSpace = init_spaces(xOrigin,yOrigin,xSize,ySize,lspace,dr,vecPer)
part,partSpace = sortPart(part,posSpace,partSpace,xOrigin,yOrigin,xSize,ySize,lspace,dr)
listNeibSpace= getListNeib(partSpace,neibSpace,listNeibSpace)
```

#### Affichage du domaine

```python
%matplotlib notebook
Umax= 1
tabUx = part[:,VEL[0]]
domain = [xOrigin,xMax,yOrigin,yMax,0,Umax]
plotPropertiesWithBound(part,tabUx,r'$U_x$',domain,dr,1)
```

    <IPython.core.display.Javascript object>

#### Affichage des espaces

```python
plotSpaces(posSpace,'k',lspace,1)
neibSpace[2][neibSpace[2]>-1]
spacesOutline(posSpace[neibSpace[2][neibSpace[2]>-1]],'r',lspace,1)
```

#### Initialisation de la vitesse de la paroi supérieure

```python
part[:,VEL] = initMobileBoundVelocity((part[:,INFO]==MOBILEBOUND), part[:,VEL], U)
```

#### Interpolation de la vitesse et de la masse volumique au bord

```python
part[:,RHO],part[:,VEL] =interpolateBoundaryPeriodicX((part[:,INFO]==BOUND),\
                                     part[:,SPID],\
                                     part[:,POS],\
                                     part[:,VEL],\
                                     part[:,RHO],\
                                     listNeibSpace,aW,h,m,B,rhoF,gamma,grav,vecPer[0],shepardMin)
```

#### Gestion des enregistrements

```python
current_directory = os.getcwd()
case_directory = os.path.join(current_directory, r'Results/Couette_'+time.strftime("%Y%m%d_%H%M%S"))
os.mkdir(case_directory)
data_directory = os.path.join(case_directory,r'Data')
figures_directory = os.path.join(case_directory,r'Figures')
os.mkdir(data_directory)
os.mkdir(figures_directory)
```

#### Gestion du pas de temps et des fréquences de sorties des figures

```python
# Here we specify the output frequencies
dt_figure = 0.025*e**2*rhoF/mu
t_print = 0
t_end = 3
# Time management
t=0
it=0
im_count=0
```

#### Programmation de la solution analytique

```python
#Analytical solution for the velocity with renormalization
ytab = np.linspace(-1,1,100)
Utab = (U/2)*(ytab/(2*e) + 1)
```

### 1.3 Simulation SPH

La fonction "CFLConditions" a du être modifiée pour tenir compte du fait que la gravité est nulle dans notre simulation.

```python
%matplotlib notebook
while t<t_end:
    #STEP1 : CFL Computation
    dt = CFLConditions(part[:,VEL],h,c0,grav,rhoF,mu)
    #STEP2 : Interpolation of the density and the velocy near bounds
    part[:,RHO],part[:,VEL] =interpolateBoundaryPeriodicX((part[:,INFO]==BOUND),\
                                     part[:,SPID],\
                                     part[:,POS],\
                                     part[:,VEL],\
                                     part[:,RHO],\
                                     listNeibSpace,\
                                 aW,h,m,B,rhoF,gamma,grav,vecPer[0],shepardMin)
    #STEP3 : Interpolation of the density and the velocy near mobile bounds
    part[:,RHO], part[:, VEL] = interpolateMobileBoundaryPeriodicX((part[:,INFO]==MOBILEBOUND),\
                                     part[:,SPID],\
                                     part[:,POS],\
                                     part[:,VEL],\
                                     np.array([U, 0.0]),\
                                     part[:,RHO],\
                                     listNeibSpace,\
                                 aW,h,m,B,rhoF,gamma,grav,vecPer[0],shepardMin)

    #STEP4 : Computation of density and forces
    part[:,FORCES],part[:,DRHODT] = computeForcesMorrisPeriodicX((part[:,INFO]==FLUID),\
                                                 part[:,SPID],\
                                                 part[:,POS],\
                                                 part[:,VEL],\
                                                 part[:,RHO],\
                                                 listNeibSpace,\
                                                 aW,h,m,B,rhoF,gamma,grav,mu,vecPer[0])
    #STEP5 : Time integration for fluid particle
    part[:,POS],part[:,VEL],part[:,RHO] = integrationStepPeriodicX((part[:,INFO]==FLUID),\
                                                       part[:,POS],\
                                                       part[:,VEL],\
                                                       part[:,RHO],\
                                                       part[:,FORCES],\
                                                       part[:,DRHODT],\
                                                       dt,vecPer[0])
    #STEP6 : Time integration for mobile bound particle
    part[:,POS], part[:,RHO] = integrationStepPeriodicX_Moving_Bound((part[:,INFO]==MOBILEBOUND),\
                                                       part[:,POS],\
                                                       part[:,VEL],\
                                                       part[:,RHO],\
                                                       part[:,FORCES],\
                                                       part[:,DRHODT],\
                                                       dt,vecPer[0])

    #STEP7 : correct particle density if too low
    part[:,RHO] = checkDensity(part[:,RHO],rhoMin,rhoMax)

    #STEP8 : Neighbours update
    part,partSpace = sortPart(part,posSpace,partSpace,xOrigin,yOrigin,xSize,ySize,lspace,dr)
    listNeibSpace= getListNeib(partSpace,neibSpace,listNeibSpace)
    t +=dt
    it +=1
    if t>=t_print:
        fig = plt.figure(1)
        plt.clf()
        plt.title(r'$t\nu/e^2 = %2.2f$'%(t*mu/rhoF/e**2))
        velMagn = (part[:,VEL[0]]*part[:,VEL[0]]+part[:,VEL[1]]*part[:,VEL[1]])**.5
        domain = [xOrigin,xMax,yOrigin,yMax,0,1]
        plotPropertiesWithBound(part,velMagn,r'$u/U_0$',domain,dr,1)
        figname = os.path.join(figures_directory,r'vel_%06d.png'%im_count)
        fig.savefig(figname,bbox_inches='tight')
        fig.canvas.draw()
        plt.pause(0.01)

        #FIGURE DISPLAY
        fig2 = plt.figure(2)
        plt.clf()
        plt.plot(part[:,POS[1]]/e,part[:,VEL[0]]/U,'bo',label=r'$U_x$ SPH')
        plt.plot(part[:,POS[1]]/e,part[:,VEL[1]]/U,'ro',label=r'$U_y$ SPH')
        plt.plot(ytab,Utab,'--k',label=r'Asymptotic')
        plt.xlabel('$y/e$',fontsize=18)
        plt.ylabel('$u/U_0$',fontsize=18)
        plt.xlim(-1-nBound*dr/e,1+nBound*dr/e)
        plt.ylim(-0.2,1.2)
        plt.legend(loc='upper left')
        plt.tight_layout()
        ax = plt.gca()
        ax.tick_params(axis = 'both', which = 'major', labelsize = 18)
        ax.xaxis.set_major_locator(plt.MaxNLocator(5))
        ax.yaxis.set_major_locator(plt.MaxNLocator(5))
        plt.tight_layout()
        plt.show(block=False)
        plt.draw()
        figname = os.path.join(figures_directory,r'UV_%06d.png'%im_count)
        fig2.savefig(figname,bbox_inches='tight')
        fig2.canvas.draw()
        plt.pause(0.01)
        im_count = im_count+1
        t_print +=dt_figure

```

### 1.4 Analyse des résultats

On observe tout d'abord que la solution numérique s'approche bien de la solution analytique. le profil de vitesse est bien linéaire selon y.

##### Ce cas test nous permet donc de valider les fonctions mises en place afin de gérer le comportement du fluide au voisinage d'une paroi solide mobile.

Cette solution est néanmoins obtenue après avoir négligé totalement la gravité. Ces deux composantes sont effectivement nulles. Lorsqu'on l'active, le fluide "se tasse" et la solution numérique s'éloigne grandement de la solution analytique. Au cours de ce projet nous avons choisi de nous concentrer sur la gestion des parois mobiles, nottament en explorant plusieurs configurations. Si ce projet devait être poursuivi, il serait intéressant de résoudre ce problème. Cela permettrait sans doute de simuler des situations diverses pour lesquelles on ne peut pas négliger l'influence de la gravité.

Après avoir validé notre code sur un cas test pour lequel on disposait d'une solution analytique; nous avons souhaité tester notre travail sur un autre cas classique, l'écoulement dans une cavité entrainée : [Lid_driven_cavity](Lid_driven_cavity.ipynb)


```python
# 그래프, 수학 기능 추가
# Add graph and math features
import pylab as py
import numpy as np
import numpy.linalg as nl
# 기호 연산 기능 추가
# Add symbolic operation capability
import sympy as sy

```

# 벡터의 내적과 외적
Inner and Cross Products of Vectors

## 벡터의 내적
Inner Product of Vectors

### `numpy`

벡터의 내적에 관해 아래 비디오 링크를 참고할 수 있다. (9:09) (내적 주요 설명 1:54 ~ 4:40)

Regarding the vector inner product, you can check the following link. (9:09) (Inner product explanation 1:54 ~ 4:40)

[](https://www.youtube.com/watch?v=WNuIhXo39_k)

예를 들어 2차원 벡터를 생각해 보자.

For example, let's think about two-dimensional vectors (2D vectors).

```python
# ref : https://www.youtube.com/watch?v=8QihetGj3pg
b = np.array((2, 5))
c = np.array((7, 1))

```

```python
b

```

```python
c

```

위 2차원 벡터를 한번 그려 보자

Let's plot the 2D vectors above.

```python
def draw_2dvec(x, y, x0=0, y0=0, color='k', name=None):
    py.quiver(x0, y0, x, y, color=color, angles='xy', scale_units='xy', scale=1)
    if name is not None:
        if not name.startswith('$'):
            vec_str = '$\\vec{%s}$' % name
        else:
            vec_str = name
        py.text(0.5 * x + x0, 0.5 * y + y0, vec_str)

```

```python
draw_2dvec(b[0], b[1], name='b')
draw_2dvec(c[0], c[1], name='c')

py.axis('equal')
py.xlim((-8, 8))
py.ylim((-8, 8))
py.grid(True)

```

이 두 벡터의 내적을 계산해 보자
Let's calculate the inner product of these two vectors.

```python
py.dot(b, c)

```

확인해 보자.
Let's verify.

```python
b[0]*c[0] + b[1]*c[1]

```

다른 방법도 있다.
Other methods are available.

```python
# https://docs.scipy.org/doc/numpy/reference/generated/numpy.inner.html
py.inner(b, c)

```

```python
# https://docs.scipy.org/doc/numpy/reference/generated/numpy.inner.html
sum(b[:] * c[:])

```

**Python 3.5, Numpy 1.10 이상**에서는 **`@` 연산자**를 내적의 의미로 사용할 수 있다.

For **Python 3.5 or higher and Numpy 1.10 or higher**, we can use **`@` operator** as inner product.

```python
b @ c

```

```python
theta_a_deg = 30
theta_a_rad = np.deg2rad(theta_a_deg)
theta_b_deg = 120
theta_b_rad = np.deg2rad(theta_b_deg)

a_row_array = np.array([np.cos(theta_a_rad), np.sin(theta_a_rad)])
b_row_array = np.array([np.cos(theta_b_rad), np.sin(theta_b_rad)])

```

```python
py.plot((0, a_row_array[0]), (0, a_row_array[1]), '-', label='a')
py.plot((0, b_row_array[0]), (0, b_row_array[1]), '-', label='b')
py.grid(True)
py.legend(loc=0)
py.axis('equal')
py.show()

```

```python
print('a dot b (array) =', np.dot(a_row_array, b_row_array))
print('a_row_array @ b_row_array =', a_row_array @ b_row_array)

```

### 표준 기능
Standard library

```python
b_list = [2, 5]
c_list = [7, 1]
print(f"b_list = {b_list}")
print(f"c_list = {c_list}")

```

```python
def dot_product_list_functional(x_list, y_list):
    return sum(xi*yi for xi, yi in zip(x_list, y_list))

```

```python
def dot_product_list_for(x_list, y_list):
    s = 0
    for xi, yi in zip(x_list, y_list):
        s += (xi * yi)
    return s

```

```python
print(f"dot_product_list_functional(b_list, c_list) = {dot_product_list_functional(b_list, c_list)}")
print(f"dot_product_list_for(b_list, c_list) = {dot_product_list_for(b_list, c_list)}")

```

## 벡터의 외적
Cross Product of Vectors

아래 비디오 링크를 참고할 수 있다. (15:46)

Regarding the vector inner product, you can check the following link. (15:46)

[](https://www.youtube.com/watch?v=pJzmiywagfY)

열벡터의 잇점은 무엇이라고 생각하는가?

What do you think is the advantage of the column vector form?

### `numpy`

다음과 같은 두 벡터를 생각해 보자.
Let's think about following two vectors.

```python
a = py.array((1, -7, 1))
b = py.array((5, 2, 4))

```

외적을 계산해 보자.
Let's calculate the cross product.

```python
a_cross_b = py.cross(a, b)

```

```python
a_cross_b

```

확인 해 보자.
Let's verify.

```python
py.dot(a, a_cross_b)

```

```python
py.dot(b, a_cross_b)

```

외적의 결과가 $\vec{a}$, $\vec{b}$ 와 수직임을 알 수 있다.
We can see that the cross product is orthogonal to $\vec{a}$ and $\vec{b}$.

### 표준기능
Standard library

```python
a_list = (1, -7, 1)
b_list = (5, 2, 4)

```

```python
def cross_product_list(x_list, y_list):
    assert 3 == len(x_list)
    assert 3 == len(y_list)
    return [
        x_list[1]*y_list[2] - x_list[2]*y_list[1],
        x_list[0]*y_list[2] - x_list[2]*y_list[0],
        x_list[0]*y_list[1] - x_list[1]*y_list[0],
    ]

```

```python
print(f"cross_product_list(a_list, b_list) = {cross_product_list(a_list, b_list)}")

```

## 행렬 벡터 곱셈
Matrix Vector Multiplication

### 표준 기능
Standard library

```python
theta_deg = 60
theta_rad = py.deg2rad(theta_deg)

c, s = py.cos(theta_rad), py.sin(theta_rad)

mat_A_list_list = [
    [c, -s],
    [s, c],
]

vec_x_list = [1, 0]
vec_y_list = [0, 1]

```

```python
def mat_mul_vec(mat_list_list, vec_list):
    assert len(mat_list_list[0]) == len(vec_list)
    result_list = []
    for row_list in mat_list_list:
        result_i = 0
        for j in range(len(vec_list)):
            result_i += row_list[j] * vec_list[j]
        result_list.append(result_i)
    return result_list

```

```python
rotated_x_list = mat_mul_vec(mat_A_list_list, vec_x_list)
rotated_x_list

```

```python
rotated_y_list = mat_mul_vec(mat_A_list_list, vec_y_list)
rotated_y_list

```

## 연습문제
Exercise

힘 벡터 (10, 10) 의 $\vec{x'}, \vec{y'}$ 방향 성분을 각각 구하시오.

$$
\begin{align}
    \vec{x'}&=\left(0.8, 0.6\right) \\
    \vec{y'}&=\left(-0.6, 0.8\right)
\end{align}
$$

힘의 작용점 $\vec{r}= (1, 1, 1)$인 힘 벡터 $\vec{f}=(-1, 1, -1)$가 원점에 작용하는 모멘트의 크기와 방향을 구하시오

## Final Bell
마지막 종

```python
# stackoverfow.com/a/24634221
import os
os.system("printf '\a'");

```

```python

```