Simulating data#
Tools#
During these exercises, we will learn some general Python and biochemistry tools:
applying numerical integration methods to calculate the change in concentration of
and with time
applying numerical integration methods to calculate the change in concentration of
, , and with time
applying numerical integration methods to calculate the change in concentration of
, , , and with time
Why?#
This taster session applies numerical integration methods to ordinary differential equations to calculate the change in concentrations for simple biochemical reactions. The same mathematical modeling can be used for biochemical pathways and networks to help us understand how e.g. environmental stimuli induce various phenotypes via sequences of biochemical reactions and time-varying concentrations of biomolecular species.
Interested? Have a look at: Aldridge, B., Burke, J., Lauffenburger, D. et al. Physicochemical modelling of cell signalling pathways. Nat Cell Biol 8, 1195–1203 (2006). https://doi.org/10.1038/ncb1497
Convention#
We will use the convention that
Import libraries#
Before we start, we need to import
the correct libraries, i.e. NumPy, Matplotlib, pandas, and the odeint
and curve_fit
functions from SciPy.
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
from scipy.integrate import odeint
from scipy.optimize import curve_fit
#
Background#
In the following, simple mechanism,
This irreversible first-order reaction is a reaction that proceeds at a reaction rate,
This is an ordinary differential equation (ODE). We want to find how the concentration of
Solving this equation analytically shows an exponential decay defined by
and because
The rate constant can be estimated from the relationship
where the half life or half time,
To solve this ODE numerically, we simply take a small but finite time interval odeint
function of the SciPy package. Briefly, odeint
requires four main parameters: odeint(model, y0, t, args=)
, with
model: a function that returns derivative values at requested y and t values: dydt = model(y, t)
y0: the initial conditions of the y states
t: the time points for which the solutions need to be reported
args: when the model function depends on variables other than y and t, these can be passed in a tuple. Of note, a tuple consists of a number of values separated by commas. It is different from a list as a tuple is a collection which is ordered and unchangeable. For more information, see here.
See also the odeint
documentation, available here.
Simulation#
We start be defining a function that returns
def funcAtoB(C, t, kf): #create the function
"""
Define a function for A -> B
Args:
C, the concentrations of A and B (float) in mol/L
t, the time (float) in s
kf, the forward rate constant (float) in 1/s
Returns:
dC/dt, derivative values (float) in (mol/L)/s
"""
CA = C[0] #the initial concentration of A is the first element from the list C
CB = C[1] #the initial concentration of B is the second element from the list C
dAdt = -kf * CA #rate equation for [A]
dBdt = +kf * CA #rate equation for [B]
return [dAdt, dBdt]
We then provide the initial concentrations, time points, and a rate constant. We use the following parameters:
initial concentration of
:initial concentration of
:rate constant:
time points: 1000 points between
and , generated usingnumpy.linspace(start, stop, num)
.
CAtoB = [100, 0] #y0 - initial conditions
tAtoB = np.linspace(0, 2, 1000) #t - time points, an array with 1000 evenly distributed elements between 0 (included) and 2 (included)
kfAtoB = (7,) #args - rate constant. Of note, a tuple with one item is constructed by following a value with a comma!
We can now solve the ODE for each time point:
yAtoB = odeint(funcAtoB, CAtoB, tAtoB, kfAtoB) #solve ODE - odeint(model, y0, t, args=)
print(yAtoB) #print the outcome
[[1.00000000e+02 0.00000000e+00]
[9.86083725e+01 1.39162750e+00]
[9.72361113e+01 2.76388871e+00]
...
[8.55163449e-05 9.99999145e+01]
[8.43263420e-05 9.99999157e+01]
[8.31528952e-05 9.99999168e+01]]
Let’s use matplotlib.pyplot.plot
to create the
plt.figure(figsize=(7,5)) #to create a figure object
plt.plot(tAtoB,yAtoB[:,0],'g') #select the first column of yAtoB, i.e. [A], and plot versus t, use green
plt.plot(tAtoB,yAtoB[:,1],'b') #select the second column of yAtoB, i.e. [B], and plot versus t, use blue
plt.xlabel('Time (s)', fontsize=18) #label the X-axis
plt.ylabel('[A] (mM)', fontsize=18) #label the Y-axis
plt.xlim([0,1]) #set X-axis range
plt.ylim([0,110]) #set Y-axis range
plt.legend(['A','B'], loc='best') #include a legend
plt.show() #show the figure object

Nice! The curve shows the signal we can expect (whether we measure the dissappearance of A or the appearance of B) for an irreversible first-order reaction
Exercise 80
How would you determine
Solution to Exercise 80
If our reactant A absorbs light at a given wavelength (and our product B does not), then we can follow the absorbance and use the Beer-Lambert law to determine the concentration of reactant A,
A plot of
to determine
When we take the natural logarithm of both sides, we obtain:
For an irreversible first-order reaction, a plot of
This is demonstrated in the following figure:

#
Background#
We now use the numerical approach to simulate the reaction profile for a reversible second-order binding reaction:
Where
The following differential equations, describing the rate of variation of
Note that we need to include a term for the reformation of
Rearranging gives the equilibrium dissociation constant
There are analytical solutions to
Exercise 81
Prove that
Solution to Exercise 81

One can also solve this set of ODEs numerically using the odeint
function of the SciPy package.
Simulation#
We start be defining a function that returns
def funcAandBtofromAB(C, t, kf, kr): #create the function
"""
Define a function for A + B -> AB
Args:
C, the concentrations of A, B, and AB (float) in mol/L
t, the time (float) in s
kf, the forward rate constant (float) in (mol/L)/s
kr, the reverse rate constant (float) in 1/s
Returns:
dC/dt, derivative values (float) in (mol/L)/s
"""
CA = C[0] #the initial concentration of A is the first element from the list C
CB = C[1] #the initial concentration of B is the second element from the list C
CAB = C[2] #the initial concentration of AB is the thirs element from the list C
dAdt = -kf * CA * CB + kr * CAB #rate equation for [A]
dBdt = -kf * CA * CB + kr * CAB #rate equation for [B]
dABdt = kf * CA * CB - kr * CAB #rate equation for [AB]
return [dAdt, dBdt, dABdt]
We then provide the initial concentrations, time points, and a rate constant. We use the following parameters:
initial concentration of
:initial concentration of
:initial concentration of
:rate constants:
; .time points: 1000 points between
and
CAandBtofromAB = [10, 50, 0] #y0 - initial conditions
tAandBtofromAB = np.linspace(0, 0.1, 1000) #t - time points, an array with 1000 evenly distributed elements between 0 (included) and 0.1 (included)
ksAandBtofromAB = (1, 10) #args - rate constants
We can now solve the ODE for each time point:
yAandBtofromAB = odeint(funcAandBtofromAB, CAandBtofromAB, tAandBtofromAB, ksAandBtofromAB) #solve ODE - odeint(model, y0, t, args=)
print(yAandBtofromAB) #print the outcome
[[1.00000000e+01 5.00000000e+01 0.00000000e+00]
[9.95012480e+00 4.99501248e+01 4.98752049e-02]
[9.90059739e+00 4.99005974e+01 9.94026072e-02]
...
[1.95837942e+00 4.19583794e+01 8.04162058e+00]
[1.95820429e+00 4.19582043e+01 8.04179571e+00]
[1.95803011e+00 4.19580301e+01 8.04196989e+00]]
We can now plot our traces.
plt.figure(figsize=(7,5)) #to create a figure object
plt.plot(tAandBtofromAB,yAandBtofromAB[:,0],'g') #select the first column of yAandBtofromAB, i.e. [A], and plot versus t, use green
plt.plot(tAandBtofromAB,yAandBtofromAB[:,1],'b') #select the second column of yAandBtofromAB, i.e. [B], and plot versus t, use blue
plt.plot(tAandBtofromAB,yAandBtofromAB[:,2],'r') #select the third column of yAandBtofromAB, i.e. [AB], and plot versus t, use red
plt.xlabel('Time (s)', fontsize=18) #label the X-axis
plt.ylabel('[x] ($\mu$M)', fontsize=18) #label the Y-axis
plt.xlim([0,0.1]) #set X-axis range
plt.ylim([0,55]) #set Y-axis range
plt.legend(['A','B', 'AB'], loc='best') #include a legend
plt.show() #show the figure object
<>:6: SyntaxWarning: invalid escape sequence '\m'
<>:6: SyntaxWarning: invalid escape sequence '\m'
C:\Users\ucbtrv2\AppData\Local\Temp\ipykernel_26512\467036247.py:6: SyntaxWarning: invalid escape sequence '\m'
plt.ylabel('[x] ($\mu$M)', fontsize=18) #label the Y-axis

Nice! The curve shows the signal we can expect (whether we measure the dissappearances of A or B or the appearance of AB) for a reversible second-order reaction
Exercise 82
How would you determine
Solution to Exercise 82
If our product AB absorbs light at a given wavelength (and our reactants A and B do not), then we can follow the absorbance and use the Beer-Lambert law to determine the concentration of product AB,
When we use “pseudo first order conditions”,
More information about pre-steady state kinetics to determine rate constants can be found in
Pollard TD, De La Cruz EM. Take advantage of time in your experiments: a guide to simple, informative kinetics assays. Mol Biol Cell. 2013;24(8):1103-1110. doi:10.1091/mbc.E13-01-0030
Johnson KA. Transient state kinetic analysis of enzyme reaction pathways. Enzymes. 1992;20:1-61.doi:10.1016/S1874-6047(08)60019-0
#
Background#
In the following, simple mechanism,
Where
We will now simulate this enzyme-catalysed reaction using a numerical approach to see how
We use the following parameters:
initial concentration of enzyme,
initial concentration of substrate,
initial concentration of enzyme-substrate complex,
initial concentration of product,
rate constants:
; ;time points: 10000 points between
and
Simulation#
Exercise 83
It is up to you to
Derive the differential equations, describing the rate of variation of
, , , and , from the reaction.Define a function that returns
, , , and .Provide rate constants, initial concentrations, and time points, and evaluate the function for each time point.
Plot the resulting time traces from 0 to 60
.Discuss the simulated data.
Solution to Exercise 83
Derive the differential equations, describing the rate of variation of
, , , and , from the reaction.
Define a function that returns
, , , and . Here, I used ; ; .
#model - function that returns dE/dt, dS/dt, dES/dt, and dP/dt
def funcEandStofromEStoP(C, t, k1, k2, k3) :
# constants
CE = C[0]
CS = C[1]
CES = C[2]
CP = C[3]
# equations
dEdt = -k1 * CE * CS + (k2 + k3) * CES
dSdt = -k1 * CE * CS + k2 * CES
dESdt = k1 * CE * CS - (k2 + k3) * CES
dPdt = k3 * CES
return [dEdt, dSdt, dESdt, dPdt]
Provide rate constants, initial concentrations, and time points, and evaluate the function for each time point.
#y0 - initial conditions
CEandStofromEStoP = [1, 10, 0, 0]
#t - time points
tEandStofromEStoP = np.linspace(0, 60, 10000)
#args - rate constants
ksEandStofromEStoP = (1, 0.1, 0.5)
#solve ODE - odeint(model, y0, t, args=)
yEandStofromEStoP = odeint(funcEandStofromEStoP, CEandStofromEStoP, tEandStofromEStoP, ksEandStofromEStoP)
print(yEandStofromEStoP)
Plot the resulting time traces from 0 to 60
.
#create plot
plt.figure(figsize=(7,5))
plt.plot(tEandStofromEStoP,yEandStofromEStoP[:,0],'g:') #select the first column of yEandStofromEStoP, i.e. [E], and plot versus t, use green
plt.plot(tEandStofromEStoP,yEandStofromEStoP[:,1],'b-') #select the second column of yEandStofromEStoP, i.e. [S], and plot versus t, use blue
plt.plot(tEandStofromEStoP,yEandStofromEStoP[:,2],'r--') #select the third column of yEandStofromEStoP, i.e. [ES], and plot versus t, use red
plt.plot(tEandStofromEStoP,yEandStofromEStoP[:,3],'y-.') #select the fourth column of yEandStofromEStoP, i.e. [P], and plot versus t, use yellow
plt.xlabel('Time (s)', fontsize=18) #label the X-axis
plt.ylabel('[x] ($\mu$M)', fontsize=18) #label the Y-axis
plt.xlim([0,60]) #set X-axis range
plt.ylim([0,11]) #set Y-axis range
plt.legend(['E','S', 'ES', 'P'], loc='best') #include a legend
plt.show()
Discuss the simulated data.
Note that the reaction velocity or rate (i.e., the slope of the
Note the three phases:
pre-steady state, with a burst in
steady state, with almost constant
, andend, where
drops. Let us analyse these three phases in detail in the following exercise.
Pre-steady state and steady state phases#
Exercise 84
Let’s analyse
The pre-steady state (or transient) phase, in which there is a burst in
. In this case between 0 and ~ 0.5 .The steady state phase, in which
remains almost constant. Here, between 0.5 and ~ 10 .After ~ 10
, drops and there is a progressively nonlinear formation of product.
Solution to Exercise 84
The pre-steady state (or transient) phase, in which there is a burst in
. In this case between 0 and ~ 0.5 .
#create plot for [E] and [S] from 0 to 0.5 s
plt.figure(figsize=(7,5))
plt.plot(tEandStofromEStoP,yEandStofromEStoP[:,0],'g:') #select the first column of yEandStofromEStoP, i.e. [E], and plot versus t, use green
plt.plot(tEandStofromEStoP,yEandStofromEStoP[:,2],'r--') #select the third column of yEandStofromEStoP, i.e. [ES], and plot versus t, use red
plt.xlabel('Time (s)', fontsize=18) #label the X-axis
plt.ylabel('[x] ($\mu$M)', fontsize=18) #label the Y-axis
plt.xlim([0,0.5]) #set X-axis range
plt.ylim([0,1.1]) #set Y-axis range
plt.legend(['E','ES'], loc='best') #include a legend
plt.show()
The steady state phase, in which
remains almost constant. Here, between 0.5 and ~ 10 .
#create plot for [E] and [S] from 0.5 to 10 s
plt.figure(figsize=(7,5))
plt.plot(tEandStofromEStoP,yEandStofromEStoP[:,0],'g:') #select the first column of yEandStofromEStoP, i.e. [E], and plot versus t, use green
plt.plot(tEandStofromEStoP,yEandStofromEStoP[:,2],'r--') #select the third column of yEandStofromEStoP, i.e. [ES], and plot versus t, use red
plt.xlabel('Time (s)', fontsize=18) #label the X-axis
plt.ylabel('[x] ($\mu$M)', fontsize=18) #label the Y-axis
plt.xlim([0.5,10]) #set X-axis range
plt.ylim([0,1.1]) #set Y-axis range
plt.legend(['E', 'ES'], loc='best') #include a legend
plt.show()
After ~ 10
, drops and there is a progressively nonlinear formation of product.
#create plot for [E] and [S] from 0 to 60 s
plt.figure(figsize=(7,5))
plt.plot(tEandStofromEStoP,yEandStofromEStoP[:,0],'g:') #select the first column of yEandStofromEStoP, i.e. [E], and plot versus t, use green
plt.plot(tEandStofromEStoP,yEandStofromEStoP[:,2],'r--') #select the third column of yEandStofromEStoP, i.e. [ES], and plot versus t, use red
plt.xlabel('Time (s)', fontsize=18) #label the X-axis
plt.ylabel('[x] ($\mu$M)', fontsize=18) #label the Y-axis
plt.xlim([0,60]) #set X-axis range
plt.ylim([0,1.1]) #set Y-axis range
plt.legend(['E','ES'], loc='best') #include a legend
plt.show()
Experimental design: relative to #
Exercise 85
Let’s further investigate what effect changing the value of
Compare
and for ; ; , , , and . We can see that the initial reaction rate decreases and that decreases rapidly for . There is only enough substrate for a single turnover. Where does each curve reach a plateau?Next, compare
and for ; ; , , , and . We can see that the duration of the steady-state phase increases for . The enzyme undergoes multiple rounds of catalysis without depleting the substrate. When we study steady-state kinetics, conditions are chosen so that .
Solution to Exercise 85
Compare
and for ; ; , , , and . We can see that the initial reaction rate decreases and that decreases rapidly for . There is only enough substrate for a single turnover. Where does each curve reach a plateau?
CEandStofromEStoP_1 = [1, 10, 0, 0]
CEandStofromEStoP_2 = [1, 1, 0, 0]
yEandStofromEStoP_1 = odeint(funcEandStofromEStoP, CEandStofromEStoP_1, tEandStofromEStoP, ksEandStofromEStoP)
yEandStofromEStoP_2 = odeint(funcEandStofromEStoP, CEandStofromEStoP_2, tEandStofromEStoP, ksEandStofromEStoP)
plt.figure(figsize=(7,5))
plt.plot(tEandStofromEStoP,yEandStofromEStoP_1[:,2],'g:') #select the third column of yEandStofromEStoP, i.e. [ES], and plot versus t, use red
plt.plot(tEandStofromEStoP,yEandStofromEStoP_2[:,2],'r--') #select the third column of yEandStofromEStoP, i.e. [ES], and plot versus t, use red
plt.xlabel('Time (s)', fontsize=18) #label the X-axis
plt.ylabel('[ES] ($\mu$M)', fontsize=18) #label the Y-axis
plt.xlim([0,30]) #set X-axis range
plt.ylim([0,1.1]) #set Y-axis range
plt.legend(['[S] = 10 $\mu$M','[S] = 1 $\mu$M'], loc='best') #include a legend
plt.show()
plt.figure(figsize=(7,5))
plt.plot(tEandStofromEStoP,yEandStofromEStoP_1[:,3],'g:') #select the fourth column of yAandBtofromAB, i.e. [P], and plot versus t, use green
plt.plot(tEandStofromEStoP,yEandStofromEStoP_2[:,3],'r--') #select the fourth column of yAandBtofromAB, i.e. [P], and plot versus t, use red
plt.xlabel('Time (s)', fontsize=18) #label the X-axis
plt.ylabel('[P] ($\mu$M)', fontsize=18) #label the Y-axis
plt.xlim([0,30]) #set X-axis range
plt.ylim([0,11]) #set Y-axis range
plt.legend(['[S] = 10 $\mu$M','[S] = 1 $\mu$M'], loc='best') #include a legend
plt.show()
In the first graph (
In the second graph, (
Next, compare
and for ; ; , , , and . We can see that the duration of the steady-state phase increases for . The enzyme undergoes multiple rounds of catalysis without depleting the substrate. When we study steady-state kinetics, conditions are chosen so that .
CEandStofromEStoP_3 = [1, 10, 0, 0]
CEandStofromEStoP_4 = [0.1, 10, 0, 0]
yEandStofromEStoP_3 = odeint(funcEandStofromEStoP, CEandStofromEStoP_3, tEandStofromEStoP, ksEandStofromEStoP)
yEandStofromEStoP_4 = odeint(funcEandStofromEStoP, CEandStofromEStoP_4, tEandStofromEStoP, ksEandStofromEStoP)
plt.figure(figsize=(7,5))
plt.plot(tEandStofromEStoP,yEandStofromEStoP_3[:,2],'g:') #select the third column of yEandStofromEStoP, i.e. [ES], and plot versus t, use red
plt.plot(tEandStofromEStoP,yEandStofromEStoP_4[:,2],'r--') #select the third column of yEandStofromEStoP, i.e. [ES], and plot versus t, use red
plt.xlabel('Time (s)', fontsize=18) #label the X-axis
plt.ylabel('[ES] ($\mu$M)', fontsize=18) #label the Y-axis
plt.xlim([0,30]) #set X-axis range
plt.ylim([0,1.1]) #set Y-axis range
plt.legend(['[S] = 1 $\mu$M','[S] = 0.1 $\mu$M'], loc='best') #include a legend
plt.show()
plt.figure(figsize=(7,5))
plt.plot(tEandStofromEStoP,yEandStofromEStoP_3[:,3],'g:') #select the fourth column of yAandBtofromAB, i.e. [P], and plot versus t, use green
plt.plot(tEandStofromEStoP,yEandStofromEStoP_4[:,3],'r--') #select the fourth column of yAandBtofromAB, i.e. [P], and plot versus t, use red
plt.xlabel('Time (s)', fontsize=18) #label the X-axis
plt.ylabel('[P] ($\mu$M)', fontsize=18) #label the Y-axis
plt.xlim([0,30]) #set X-axis range
plt.ylim([0,11]) #set Y-axis range
plt.legend(['[S] = 1 $\mu$M','[S] = 0.1 $\mu$M'], loc='best') #include a legend
plt.show()
From the first (
Cytochrome c#
Think about a possible mechanism for cytochrome c peroxidase activity. Don’t forget that native cytochrome c needs to be activated to become a more active peroxidase and that cytochrome c peroxidase can become deactivated as well! Which parameters do you need to be able to simulate the reaction?
There is no right answer here! For examples of possible mechanisms for cytochrome c peroxidase activity, see
Parakra RD , Kleffmann T , Jameson GNL , Ledgerwood EC . The proportion of Met80-sulfoxide dictates peroxidase activity of human cytochrome c. Dalton Trans. 2018;47(27):9128-9135. doi:10.1039/c8dt02185f
Yin V, Shaw GS, Konermann L. Cytochrome c as a Peroxidase: Activation of the Precatalytic Native State by H2O2-Induced Covalent Modifications. J Am Chem Soc. 2017;139(44):15701-15709. doi:10.1021/jacs.7b07106