You are on page 1of 12

Supporting Information

Introduction to Stochastic Simulations for Chemical and Physical


Processes: Principles and Applications

Charles J. Weiss

Department of Chemistry, Wabash College, Crawfordsville, IN 47933


Software Instructions

The software used in these simulations is free, open source software which runs on Mac,
Windows, and Linux systems. The Anaconda installer provided by Continuum Analytics has been used
by the author as a quick and convenient way of installing all of the software and libraries required.
While the following instructions are current as of this writing, be aware that the steps or details may
since change.

Installation

1. Download the appropriate graphical Anaconda installer for your system from
https://www.continuum.io/downloads. Multiple versions of Python are available. Be sure to
select the most recent version of Python 3.1
2. Click on the downloaded file and follow the installation instructions that appear.

Getting Started

1. Click on the Anaconda-Navigator icon (green circle) on your computer to start Anaconda.

2. Click Launch under Jupyter notebook.

3. Jupyter should launch in your default web browser and a terminal window will also appear. If
the browser does not launch with Jupyter, you may need to copy and paste the local host
address from the terminal window into the address bar of your web browser. The address looks
something like the following:
http://localhost:8888/?token=asdt4d93kck2kk195ieme34ifk3ifi3jfm53g

1 Python 2 is also available, but this is technically legacy and should only be used if you know that you need this version.

S2
4. From the Jupyter window (in the web browser), navigate the file system and select a Jupyter
notebook to open it. Note: double clicking a Jupyter notebook from your computer’s file
browser will not open the file; it must be opened from within Jupyter.

5. There are two types of cells in a Jupyter notebook, code cells and markdown cells. Code cells
contain live code while markdown cells contain explanatory text, images, and equations to
provide instructions, background information on the code, and/or discussion of the results.

To run a cell, select Cell → Run Cells from the menu at the top of the screen or click the Run
Cell button (looks like a play button).

S3
Alternatively, the user can create a new Jupyter notebook by selecting New → Python 3 from the
menu at the top right of the browser.

S4
Chain-Growth Polymerization
student instructions

Chain-growth polymerization is a class of polymerization mechanisms where monomers are


added one at a time to the end of a growing polymer chain until the chain terminates. Because
termination occurs at random times during the polymerization process, polymer chains of a variety of
lengths are observed. The goal of this exercise is to:
1. Stochastically simulate the chain-growth polymerization/termination process
2. Examine the resulting distribution of chain lengths
3. Compare the stochastic simulation results agains the theoretical model (described below)
Polymer chain termination can be caused by many things depending upon the mechanism of
polymerization. For radical polymerization, the polymer chain stops growing due to a radical
termination step. If the polymer is created using an organometallic catalyst, chain termination occurs
when the polymer chain detaches from the catalyst.

Instructions
In groups of two students, open the Jupyter notebook provided or a new notebook and perform
the following tasks.

1. Stochastically simulate the chain-growth process using a random value generator with a
probability argument (e.g., Poisson distribution) to dictate termination. You are welcome to
develop your own method or use the one illustrated below. It is recommended that you use ten
thousand steps/cycles in your simulation and a hundred thousand polymer chains.
First Cycle
chains 0 0 0 0 0 0 0 0 growth
chains 1 1 1 1 1 1 1 1
poly_growth 1 1 1 1 1 1 1 1

termination
poly_growth 1 1 1 1 1 1 1 1 poly_growth 1 1 1 0 1 1 1 1

Second Cycle
chains 1 1 1 1 1 1 1 1 growth
chains 2 2 2 1 2 2 2 2
poly_growth 1 1 1 0 1 1 1 1

termination
poly_growth 1 1 1 0 1 1 1 1 poly_growth 0 1 1 0 1 1 1 1

Third Cycle
chains 2 2 2 2 2 2 2 2 growth
chains 2 3 3 2 3 3 3 3
poly_growth 0 1 1 0 1 1 1 1

termination
poly_growth 0 1 1 0 1 1 1 1 poly_growth 0 1 1 0 0 1 1 1

S5
2. Generate a histogram plot showing the distribution of polymer chain lengths.
3. It is common in polymer chemistry to plot the molecular weight distribution in terms of weight
fraction (W) which is the fraction of the total polymer weight that each chain length contributes
to the over weight of polymer. This can be calculated by dividing the total weight of polymer
chains of a given length by the total weight of polymer.
4. Compare the results of the stochastic simulation against the theoretical model 2 shown below.
During each step of the polymerization, there is a given probability, p, that a polymer will add a
new monomer instead of terminating described by the below equation where ν p is the rate of
polymerization and νt is the rate of termination.3 This probability can be used to calculate the
weight fraction (W) for each molecular weight (M). To be consistent, the following plot is
plotted in terms of number of monomers (X) where M 0 is the monomer molecular weight.

2 X−1
W =(1−p) Xp
vp
where X =M / M 0 and p=
v p + vt

2 (a) Flory, P. J. Molecular Size Distribution in Linear Condensation Polymers . J. Am. Chem. Soc. 1936, 58 (10), 1877–
1885. (b) Flory, P. J. Molecular Size Distribution in Ethylene Oxide Polymers J. Am. Chem. Soc. 1940, 62 (6), 1561–
1565.
3 Horta, A.; Pastoriza, M., A. The Molecular Weight Distribution of Polymer Samples J. Chem. Educ. 2007, 84 (7), 1217-
1221.

S6
Code from Jupyter Notebooks

The code from the Jupyter notebooks is included below for convenience for anyone who wants
to review or use the code outside of the Jupyter environment. The following code includes the imports
(i.e., activation of modules/libraries), simulations, and plotting the results of the simulations.

DNA Simulation
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline
sns.set(font_scale=2, style='white')

length = 1000 # length of DNA strands


strands = 10000 # number of strands

bases = ('A', 'T', 'C', 'G')


occurrences = [] # occurrences of GCAT in each sequence

for strand in range(strands):


# make random strand
sequence = np.random.choice(bases, size=length)

# count occurrences of GCAT in strand


counter = 0
for nuc in range(length):
if list(sequence[nuc:nuc + 4]) == ['G', 'C', 'A', 'T']:
counter += 1

occurrences.append(counter) # log occurrences of GCAT

print(np.mean(occurrences)) # mean occurrences of GCAT in DNA strands

2D Brownian Motion
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline
sns.set(font_scale=2, style='white')

steps = 10000

# steps to iterate over


t = np.arange(1, steps)

# generates an empty array to store positions


position = np.empty((steps, 2))

S7
for step in t:
# random move x direction
position[step, 0] = position[step - 1, 0] + np.random.randint(-1, 2)
# random move x direction
position[step, 1] = position[step - 1, 1] + np.random.randint(-1, 2)

plt.plot(position[:,0], position[:,1], '-')


plt.xlabel('Position(x), au')
plt.ylabel('Position(y), au')
plt.plot(0,0, 'o', color='k')
plt.tight_layout()

3D Brownian Motion
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline
sns.set(font_scale=2, style='white')
from mpl_toolkits.mplot3d import Axes3D

steps = 10000

# steps to iterate over


t = np.arange(1, steps)

position = np.empty((steps, 3))

# only need to do 49 steps being that we already know that we start at (0, 0)
for step in t:
position[t, 0] = position[t - 1, 0] + np.random.randint(-1, 2)
position[t, 1] = position[t - 1, 1] + np.random.randint(-1, 2)
position[t, 2] = position[t - 1, 2] + np.random.randint(-1, 2)

fig = plt.figure(figsize=(8,8))
ax1 = fig.add_subplot(1,1,1, projection='3d')
ax1.plot(position[:,0], position[:,1],position[:,2], '-')
ax1.plot([0],[0],[0], 'o', color='k')
ax1.set_xlabel('Position(x), au', labelpad=15)
ax1.set_ylabel('Position(y), au', labelpad=15)
ax1.set_zlabel('Position(z), au', labelpad=17)
ax1.w_xaxis.set_pane_color((0, 0, 0, 0))
ax1.w_yaxis.set_pane_color((0, 0, 0, 0))
ax1.w_zaxis.set_pane_color((0, 0, 0, 0))
plt.tight_layout()

S8
Diffusion
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import scipy.stats
import seaborn as sns
%matplotlib inline
sns.set(font_scale=2, style='white')

particles = 10000 # number of particles


steps = 1000 # steps in simulation

# steps to iterate over


t = np.arange(0, steps)

loc = np.zeros(particles) # particle locations

for frame in t:
# add random value to locations
loc += 2 * (np.random.rand(particles) – 0.5)

plt.hist(loc, bins = np.arange(np.min(loc),np.max(loc),2), edgecolor='k', lw=0.1)


plt.xlabel('Distance from Origin, au')
plt.ylabel('Number of Particles')

First-Order Chemical Kinetics


%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
sns.set(font_scale=2, style='white')

steps = 10000 # length of simulation


size = 1000 # number of molecules

# steps to iterate over


t = np.arange(0, steps)

# an array that represents molecules of A


molecules = np.ones(size)

# an array to hold the "concentration" values from the simulation


conc = np.zeros(steps)

for step in t:
rand_index = np.random.randint(size) # randomly selects position
molecules[rand_index] = 0 # assigns value to zero
conc[step] = np.sum(molecules) # records number of ones

S9
plt.plot(t, conc/size) # plotted per 1000 molecules in the simulation
plt.xlabel('Iterations')
plt.ylabel('[A]/[A]o')

Competitive First-Order Kinetics


%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
sns.set(font_scale=2, style='white')

steps = 5000 # length of simulation


size = 500 # number of reactant molecules in simulation

# steps to iterate over


t = np.arange(1, steps)

molecules = np.zeros(size, dtype=np.int)

# array to track concentrations of A, B, and C during the simulation


conc = np.zeros((steps, 3), dtype=np.int)
conc[0,:] = np.array([500, 0, 0])

for step in t:
# generate indices using random number generators
index_B = np.random.randint(0, high=size)
index_C = np.random.randint(0, high=size)

# A -> B reaction
if molecules[index_B] == 0:
molecules[index_B] = 1

# A -> C reaction
if molecules[index_C] == 0 and np.random.randint(0,2) == 1:
molecules[index_C] = 2

# sort, count, and record values


conc[step, :] = np.bincount(molecules, minlength=3)

plt.plot(np.arange(steps), conc[:,0], '-', label = 'Reactant A')


plt.plot(np.arange(steps), conc[:,1], '--', label = 'Product B')
plt.plot(np.arange(steps), conc[:,2], '-.', label = 'Product C')
plt.legend(loc = 'best')
plt.xlabel('Simulation Frame')
plt.ylabel('Number of Molecules')

S10
Chain-Growth Polymerization
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
sns.set(font_scale=2, style='white')

N = 100000 # polymer chains


steps = 10000 # length simulation
p = 0.999 # probability of chain growth versus termination

# steps to iterate over


t = np.arange(0, steps)

chains = np.zeros(N) # length of chains


poly_growth = np.ones(N) # live vs terminated chains

for steps in t:
chains += poly_growth # grows the polymers
poly_growth *= np.random.binomial(1, p, size=N) # terminates chains

total_weight = np.sum(chains)

hist, bin_edges = np.histogram(chains, bins=1000)


weight_dist = (hist * (bin_edges[0:-1] + bin_edges[1:])/2) / total_weight
plt.bar(bin_edges[1:], weight_dist, width=10)
plt.xlabel('Molecular Weight, M')
plt.ylabel('Molecular Weight Distribution')

S11
Monte Carlo Calculation of Pi

Image a circle (radius = r) is inscribed inside a square of side length 2r. The area of a circle is
described by Acircle = πr2 whereas the area of the unit square is Asquare = 2r × 2r. We can calculate the ratio
of the areas of the circle and square as π/4.

A circle π r 2 π r 2 π
= = =
A square (2r )2 4 r2 4
The ratio of the two areas can be estimated by generating random points in both the circle and square,
totaling these points up, and dividing the two. This ratio can then be used to calculate a value of π. The
following code performs this simulation.

import numpy as np

def estimate_pi(samples):
'''(integer) -> pi, coordinates

Estimates the value of pi by finding the ratio of the area of a unit


circle/area of a unit square
and multiplying by 4. To simplify the calculation, it uses only a quarter of
the circle and square.
'''

coords = np.random.rand(samples,2) # random x, y coordinates

dist = np.hypot(coords[:,0], coords[:,1]) # distance from the origin


in_circle = dist[dist <= 1] # if <= 1, the point is inside the circle

pi = 4 * (np.size(in_circle)/samples)
return (pi, coords)

estimate_pi(1000)

S12

You might also like