Protein Folding with Intermediates
Contents
Protein Folding with Intermediates#
Deterministic equations#
A general set of chemical reactions can be described by the stoichiometry matrices \(R\) and \(P\) as:
The general rate equations are:
where \(Q \equiv P - R\) is the net stoichiometry matrix and \(v_\mu\) is the reaction rate that we have calculated for stochastic simulations:
Here is a general purpose class for solving rate equations.
import numpy as np
import matplotlib.pyplot as plt
import scipy.integrate as intgr
class RateEquations:
"""
solve rate equations numerically, assuming mass action kinetics.
"""
def __init__(self, stoich, rates, init, record=True):
"""
initialize solver by assigning stoichiometry matrices, rate constants, and initial values.
inputs:
stoich: 2-tuple, stoichiometry matrices of reactants and products, (R_ui, P_ui), u = 1 ~ m, i = 1 ~ n
rates: list, rate parameters, K_u, u = 1 ~ M
init: list, initial numbers of every species, N_i(0), i = 1 ~ n
record: boolean, whether to record numbers of species at time points
"""
self.reactants = np.asarray(stoich[0]) # stoichiometry matrices of reactants
self.products = np.asarray(stoich[1]) # stoichiometry matrices of products
self.rates = np.asarray(rates, dtype=float) # reaction rates
self.numbers = np.asarray(init, dtype=float) # current numbers of each species, treat as real numbers
self.num_reac = self.reactants.shape[0] # number of reactions
self.num_spec = self.reactants.shape[1] # number of species
self.time = 0. # time since beginning of simulation
self.record = record # whether to record time series
if self.record:
self.time_hist = [0.] # list of time points
self.numbers_hist = [self.numbers.copy()] # list of species numbers at time points
def run(self, tmax, dt):
"""
solve rate equations until time `tmax` since the beginning.
inputs:
tmax: float, time since the beginning of the simulation.
dt: float, time step by which solution is calculated
"""
T = tmax - self.time # time remaining to be solved
new_times = np.arange(0, T+dt, dt) # new time points at every step dt
x0 = self.numbers # current species numbers as initial values to the solver
sol = intgr.odeint(self.mass_action, x0, new_times) # solve equations using integrator
if self.record:
self.time_hist.extend(self.time + new_times[1:]) # save time points
self.numbers_hist.extend(sol[1:]) # save species numbers at given time points
self.time += new_times[-1] # update time to latest
self.numbers = sol[-1] # update species numbers to latest
def mass_action(self, x, t):
"""
calculate time derivative of species numbers using mass action kinetics.
inputs:
x: 1-d array, current numbers of every species, treat as real numbers.
t: float, current time.
outputs:
dxdt: 1-d array, time derivatives of species numbers.
"""
r_u = self.rates * np.prod(np.power(x, self.reactants), axis=1) # rates of every reaction
Q_ui = self.products - self.reactants # net stoichiometry matrix
dn_i = np.dot(r_u, Q_ui) # time derivatives of species numbers
return dn_i
Folding-unfolding model#
Let us first test this RateEquations
class on the simple folding-unfolding model we solved last time:
Now we can define a derived class for our problem by specifying the stoichiometry matrices, like what we did for stochastic simulations.
class FoldingUnfolding2(RateEquations):
"""
define a derived class to model the folding and unfolding of proteins.
"""
def __init__(self, rates, init, record=True): # decorate base method
"""
modify the initialization to specify the stoichiometry matrices.
the chemical species are U and F in that order; the reactions are U -> F and F -> U.
"""
reactants = [[1, 0],
[0, 1]]
products = [[0, 1],
[1, 0]]
RateEquations.__init__(self, (reactants, products), rates, init, record=record)
kf = 9. # folding rate
ku = 1. # unfolding rate
NU0 = 100 # initial number of unfolded proteins
NF0 = 0 # initial number of folded proteins
N_tot = NU0 + NF0 # total number of proteins
K = kf / ku # equilibrium constant
NUeq = 1/(1+K) * N_tot # equilibrium number of unfolded proteins
NFeq = K/(1+K) * N_tot # equilibrium number of folded proteins
T = 1.0
dt = 0.01
fu2 = FoldingUnfolding2([kf, ku], [NU0, NF0])
fu2.run(T, dt)
sol = np.array(fu2.numbers_hist)
fig, ax = plt.subplots(1,2, figsize=(12,4))
ax[0].axhline(NUeq, color='k', linewidth=2, label='equilibrium') # expected number at equilibrium
ax[1].axhline(NFeq, color='k', linewidth=2, label='equilibrium') # expected number at equilibrium
ax[0].plot(fu2.time_hist, sol[:,0], 'k--', label='solution') # solution to the rate equations
ax[1].plot(fu2.time_hist, sol[:,1], 'k--', label='solution') # solution to the rate equations
ax[0].set_ylim(-2, 102)
ax[0].set_xlabel('time')
ax[0].set_ylabel('#unfolded')
ax[0].legend(loc='upper right')
ax[1].set_ylim(-2, 102)
ax[1].set_xlabel('time')
ax[1].set_ylabel('#folded')
ax[1].legend(loc='lower right')
plt.show()
Folding-unfolding with an intermediate#
Let us consider a slightly more complicated model of protein folding, where the transition from the unfolded state to the folded state has to go through an intermediate state that is metastable. Denote the intermediate state by \(M\), then there will be four reactions:
Exercise: solve the extended model using rate equations.#
km = 9. # folding rate of intermediate
ku = 1. # unfolding rate of intermediate
kf = 3. # folding rate of final state
kd = 0.3 # unfolding rate of final state
NU0 = 100 # initial number of unfolded proteins
NF0 = 0 # initial number of folded proteins
NM0 = 0 # initial number of intermediate state
N_tot = NU0 + NF0 + NM0 # total number of proteins
class FoldingUnfoldingWithIntermediate(RateEquations):
"""
define a derived class to model the folding and unfolding of proteins.
"""
def __init__(self, rates, init, record=True): # decorate base method
"""
modify the initialization to specify the stoichiometry matrices.
the chemical species are U and F in that order; the reactions are U -> F and F -> U.
"""
reactants = ...
products = ...
RateEquations.__init__(self, (reactants, products), rates, init, record=record)
T = 2.0
dt = 0.01
fu2 = ...
time_hist = np.array(fu2.time_hist)
num_hist = np.array(fu2.numbers_hist)
fig, ax = plt.subplots(1,3, figsize=(18,4))
for i in range(3):
ax[i].plot(time_hist, num_hist[:,i], 'k--', label='solution') # solution to the rate equations
ax[i].set_ylim(-2, 102)
ax[i].set_xlabel('time')
ax[i].legend()
ax[0].set_ylabel('#unfolded')
ax[1].set_ylabel('#intermediate')
ax[2].set_ylabel('#folded')
plt.show()
Challenge: Calculate the equilibrium numbers in terms of the rate constants and compare to numerical results#
Solution:#
class FoldingUnfoldingWithIntermediate(RateEquations):
"""
define a derived class to model the folding and unfolding of proteins.
"""
def __init__(self, rates, init, record=True): # decorate base method
"""
modify the initialization to specify the stoichiometry matrices.
the chemical species are U and F in that order; the reactions are U -> F and F -> U.
"""
reactants = [[1, 0, 0],
[0, 1, 0],
[0, 0, 1],
[0, 1, 0]]
products = [[0, 1, 0],
[0, 0, 1],
[0, 1, 0],
[1, 0, 0]]
RateEquations.__init__(self, (reactants, products), rates, init, record=record)
T = 2.0 # total amount of time to solve
dt = 0.01 # time step by which the solutions are recorded
fu2 = FoldingUnfoldingWithIntermediate([km, kf, kd, ku], [NU0, NM0, NF0])
fu2.run(T, dt)
K1 = kf / kd
K2 = ku / km
NUeq = K2 / (1 + K1 + K2) * N_tot # equilibrium number of unfolded proteins
NMeq = 1 / (1 + K1 + K2) * N_tot # equilibrium number of intermediates
NFeq = K1 / (1 + K1 + K2) * N_tot # equilibrium number of folded proteins
time_hist = np.array(fu2.time_hist)
num_hist = np.array(fu2.numbers_hist)
fig, ax = plt.subplots(1,3, figsize=(18,4))
ax[0].axhline(NUeq, color='k', linewidth=2, label='equilibrium') # expected number at equilibrium
ax[1].axhline(NMeq, color='k', linewidth=2, label='equilibrium') # expected number at equilibrium
ax[2].axhline(NFeq, color='k', linewidth=2, label='equilibrium') # expected number at equilibrium
for i in range(3):
ax[i].plot(time_hist, num_hist[:,i], 'k--', label='solution') # solution to the rate equations
ax[i].set_ylim(-2, 102)
ax[i].set_xlabel('time')
ax[i].legend()
ax[0].set_ylabel('#unfolded')
ax[1].set_ylabel('#intermediate')
ax[2].set_ylabel('#folded')
plt.show()