4.4.8. Cooperative Binding Kinetics#
4.4.8.1. Motivation#
Some processes involving enzymes do not follow Michaelis-Menten kinetics. One such example is cooperative binding in which the binding of one molecule affects the binding of another molecule. In this notebook we will look at the ability of an enzyme to bind multiple copies of the same molecule.
4.4.8.2. Learning Goals#
After working through this notebook, you will be able to
Write out the cooperative binding mechanism
Derive the cooperative binding rate law
Identify cooperative binding behavior in a plot of \(v_0\) vs \([S]_0\)
Identify negative and postive cooperative binding behavior.
4.4.8.3. Coding Concepts#
The following coding concepts are used in this notebook:
4.4.8.4. Cooperative Binding#
One well-known example of a process that does not follow Michaelis-Menten kinetics is the binding of oxygen to hemoglobin. The binding of one oxygen molecule to Hemoglobin enhances the binding rate of subsequent oxygen molecules. Hemoglobins are actually heterotetramers can bind up to four oxygen molecules.
An example mechanism for this process is the simultaneous binding of \(n\) substrate molecules given by
where \(n\) is the number of substrates and \(ES_n\) denotes the enzyme substrate complex with \(n\) substrates bound.
This type of behavior can be observed in a \(v_0\) vs \([S]_0\) plot by distinctly sigmoidal behavior and poor ability to fit the data with the Michaelis-Menten equation.
Show code cell source
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
# setup plot parameters
fontsize=16
fig = plt.figure(figsize=(8,8), dpi= 80, facecolor='w', edgecolor='k')
ax = plt.subplot(111)
ax.grid(which='major', axis='both', color='#808080', linestyle='--')
ax.set_ylabel("$v_0$ ($\mu$M/s)",size=fontsize)
ax.set_xlabel("$[S]_0$ ($\mu$M)",size=fontsize)
plt.tick_params(axis='both',labelsize=fontsize)
def mm(S0,vmax,Km):
return vmax*S0/(Km + S0)
def hill(S0,vmax,Km,n):
return vmax*S0**n/(Km + S0**n)
vmax = 0.001
Km = 10
S0 = np.arange(0,10,0.1)
ax.plot(S0,mm(S0,vmax,Km),'--',lw=3,label="Michaelis-Menten")
ax.plot(S0,hill(S0,vmax,Km,2),'-',lw=3,label="Non-Michaelis-Menten")
plt.legend(fontsize=fontsize);
4.4.8.5. Derivation of the Hill Equation#
The rate law stemming from the above mechanism can be derived in a similar fashion to the derivation for Michaelis-Menten. To make the derivation slightly easier, we will assume the second step is irreversible
We start by writing the differential expressions for substrate and enzyme-substrate complex
we can again plug in \([E] = [E]_0 - [ES_n]\) to get
Using the steady-state approximation for \([ES_n]\) we get
Plugging this into the rate equation
where \(K_m = \frac{k_2+k_{-1}}{k_1}\) is defined similar to the Michaelis-Menten constant.
We see that the resulting initial rate law is very similar to the Michaelis-Menten rate law. Indeed, they are identical for \(n=1\) as they should be (the mechanism is then the same).
The final Hill equation is given as
where \(n\) is referred to as the Hill number. While it is appealing to interpret \(n\) as the number of substrates/ligands that bind to the enzyme, the Hill mechanism is so unphysical (simultaneous binding) that this interpretation is not necessarily true. Nonetheless, this mechanism and equation are used to fit enzyme kinetics and determine whether an enzyme displays cooperativity (negative or positive) or not.
Show code cell source
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
# setup plot parameters
fontsize=16
fig = plt.figure(figsize=(8,8), dpi= 80, facecolor='w', edgecolor='k')
ax = plt.subplot(111)
ax.grid(which='major', axis='both', color='#808080', linestyle='--')
ax.set_ylabel("$v_0$ ($\mu$M/s)",size=fontsize)
ax.set_xlabel("$[S]_0$ ($\mu$M)",size=fontsize)
plt.tick_params(axis='both',labelsize=fontsize)
def hill(S0,vmax,Km,n):
return vmax*S0**n/(Km + S0**n)
vmax = 0.001
Km = 2
S0 = np.arange(0,10,0.1)
ax.plot(S0,hill(S0,vmax,Km,1),'-',lw=3,label="n=1")
ax.plot(S0,hill(S0,vmax,Km,2),'-',lw=3,label="n=2")
ax.plot(S0,hill(S0,vmax,Km,3),'-',lw=3,label="n=3")
ax.plot(S0,hill(S0,vmax,Km,4),'-',lw=3,label="n=4")
ax.plot(S0,hill(S0,vmax,Km,5),'-',lw=3,label="n=5")
ax.plot(S0,hill(S0,vmax,Km,6),'-',lw=3,label="n=6")
plt.legend(fontsize=fontsize);
4.4.8.6. Positive and Negative Cooperativity#
Both positive and negative cooperativity can be observed. Negative cooperativity simply means that binding of one ligand negatively impacts the binding of the second ligand. There is still cooperativity but the impact is to negate the binding of the second ligand. The Hill equation can be used to fit this type of behavior and it will be observed for values of \(n<1\). Let’s look at a plot of different \(n\) values.
Show code cell source
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
# setup plot parameters
fontsize=16
fig = plt.figure(figsize=(8,8), dpi= 80, facecolor='w', edgecolor='k')
ax = plt.subplot(111)
ax.grid(which='major', axis='both', color='#808080', linestyle='--')
ax.set_ylabel("$v_0$ ($\mu$M/s)",size=fontsize)
ax.set_xlabel("$[S]_0$ ($\mu$M)",size=fontsize)
plt.tick_params(axis='both',labelsize=fontsize)
def hill(S0,vmax,Km,n):
return vmax*S0**n/(Km + S0**n)
vmax = 0.001
Km = 2
S0 = np.arange(0,1000000,0.1)
ax.plot(S0,hill(S0,vmax,Km,1),'-',lw=3,label="n=1")
ax.plot(S0,hill(S0,vmax,Km,2),'-',lw=3,label="n=2")
ax.plot(S0,hill(S0,vmax,Km,3),'-',lw=3,label="n=3")
ax.plot(S0,hill(S0,vmax,Km,4),'-',lw=3,label="n=4")
ax.plot(S0,hill(S0,vmax,Km,0.1),'-',lw=3,label="n=0.1")
ax.plot(S0,hill(S0,vmax,Km,0.2),'-',lw=3,label="n=0.2")
ax.plot(S0,hill(S0,vmax,Km,0.3),'-',lw=3,label="n=0.3")
ax.plot(S0,hill(S0,vmax,Km,0.4),'-',lw=3,label="n=0.4")
ax.plot(S0,hill(S0,vmax,Km,0.5),'-',lw=3,label="n=0.5")
plt.legend(fontsize=fontsize);
Error in callback <function flush_figures at 0x7fdb60409b80> (for post_execute):
---------------------------------------------------------------------------
KeyboardInterrupt Traceback (most recent call last)
~/opt/anaconda3/lib/python3.9/site-packages/matplotlib_inline/backend_inline.py in flush_figures()
124 # ignore the tracking, just draw and close all figures
125 try:
--> 126 return show(True)
127 except Exception as e:
128 # safely show traceback if in IPython, else raise
~/opt/anaconda3/lib/python3.9/site-packages/matplotlib_inline/backend_inline.py in show(close, block)
88 try:
89 for figure_manager in Gcf.get_all_fig_managers():
---> 90 display(
91 figure_manager.canvas.figure,
92 metadata=_fetch_figure_metadata(figure_manager.canvas.figure)
~/opt/anaconda3/lib/python3.9/site-packages/IPython/core/display.py in display(include, exclude, metadata, transient, display_id, *objs, **kwargs)
318 publish_display_data(data=obj, metadata=metadata, **kwargs)
319 else:
--> 320 format_dict, md_dict = format(obj, include=include, exclude=exclude)
321 if not format_dict:
322 # nothing to display (e.g. _ipython_display_ took over)
~/opt/anaconda3/lib/python3.9/site-packages/IPython/core/formatters.py in format(self, obj, include, exclude)
178 md = None
179 try:
--> 180 data = formatter(obj)
181 except:
182 # FIXME: log the exception
~/opt/anaconda3/lib/python3.9/site-packages/decorator.py in fun(*args, **kw)
230 if not kwsyntax:
231 args, kw = fix(args, kw, sig)
--> 232 return caller(func, *(extras + args), **kw)
233 fun.__name__ = func.__name__
234 fun.__doc__ = func.__doc__
~/opt/anaconda3/lib/python3.9/site-packages/IPython/core/formatters.py in catch_format_error(method, self, *args, **kwargs)
222 """show traceback on failed format call"""
223 try:
--> 224 r = method(self, *args, **kwargs)
225 except NotImplementedError:
226 # don't warn on NotImplementedErrors
~/opt/anaconda3/lib/python3.9/site-packages/IPython/core/formatters.py in __call__(self, obj)
339 pass
340 else:
--> 341 return printer(obj)
342 # Finally look for special method names
343 method = get_real_method(obj, self.print_method)
~/opt/anaconda3/lib/python3.9/site-packages/IPython/core/pylabtools.py in print_figure(fig, fmt, bbox_inches, base64, **kwargs)
149 FigureCanvasBase(fig)
150
--> 151 fig.canvas.print_figure(bytes_io, **kw)
152 data = bytes_io.getvalue()
153 if fmt == 'svg':
~/opt/anaconda3/lib/python3.9/site-packages/matplotlib/backend_bases.py in print_figure(self, filename, dpi, facecolor, edgecolor, orientation, format, bbox_inches, pad_inches, bbox_extra_artists, backend, **kwargs)
2297 if bbox_inches:
2298 if bbox_inches == "tight":
-> 2299 bbox_inches = self.figure.get_tightbbox(
2300 renderer, bbox_extra_artists=bbox_extra_artists)
2301 if pad_inches is None:
~/opt/anaconda3/lib/python3.9/site-packages/matplotlib/figure.py in get_tightbbox(self, renderer, bbox_extra_artists)
1682
1683 for a in artists:
-> 1684 bbox = a.get_tightbbox(renderer)
1685 if bbox is not None and (bbox.width != 0 or bbox.height != 0):
1686 bb.append(bbox)
~/opt/anaconda3/lib/python3.9/site-packages/matplotlib/axes/_base.py in get_tightbbox(self, renderer, call_axes_locator, bbox_extra_artists, for_layout_only)
4673 # this artist
4674 continue
-> 4675 bbox = a.get_tightbbox(renderer)
4676 if (bbox is not None
4677 and 0 < bbox.width < np.inf
~/opt/anaconda3/lib/python3.9/site-packages/matplotlib/legend.py in get_tightbbox(self, renderer)
913 def get_tightbbox(self, renderer):
914 # docstring inherited
--> 915 return self._legend_box.get_window_extent(renderer)
916
917 def get_frame_on(self):
~/opt/anaconda3/lib/python3.9/site-packages/matplotlib/offsetbox.py in get_window_extent(self, renderer)
349 # docstring inherited
350 w, h, xd, yd, offsets = self.get_extent_offsets(renderer)
--> 351 px, py = self.get_offset(w, h, xd, yd, renderer)
352 return mtransforms.Bbox.from_bounds(px - xd, py - yd, w, h)
353
~/opt/anaconda3/lib/python3.9/site-packages/matplotlib/offsetbox.py in get_offset(self, width, height, xdescent, ydescent, renderer)
288
289 """
--> 290 return (self._offset(width, height, xdescent, ydescent, renderer)
291 if callable(self._offset)
292 else self._offset)
~/opt/anaconda3/lib/python3.9/site-packages/matplotlib/legend.py in _findoffset(self, width, height, xdescent, ydescent, renderer)
587
588 if self._loc == 0: # "best".
--> 589 x, y = self._find_best_position(width, height, renderer)
590 elif self._loc in Legend.codes.values(): # Fixed location.
591 bbox = Bbox.from_bounds(0, 0, width, height)
~/opt/anaconda3/lib/python3.9/site-packages/matplotlib/legend.py in _find_best_position(self, width, height, renderer, consider)
1028 # XXX TODO: If markers are present, it would be good to take them
1029 # into account when checking vertex overlaps in the next line.
-> 1030 badness = (sum(legendBox.count_contains(line.vertices)
1031 for line in lines)
1032 + legendBox.count_contains(offsets)
~/opt/anaconda3/lib/python3.9/site-packages/matplotlib/legend.py in <genexpr>(.0)
1028 # XXX TODO: If markers are present, it would be good to take them
1029 # into account when checking vertex overlaps in the next line.
-> 1030 badness = (sum(legendBox.count_contains(line.vertices)
1031 for line in lines)
1032 + legendBox.count_contains(offsets)
~/opt/anaconda3/lib/python3.9/site-packages/matplotlib/transforms.py in count_contains(self, vertices)
604 vertices = np.asarray(vertices)
605 with np.errstate(invalid='ignore'):
--> 606 return (((self.min < vertices) &
607 (vertices < self.max)).all(axis=1).sum())
608
~/opt/anaconda3/lib/python3.9/site-packages/numpy-1.25.0-py3.9-macosx-10.9-x86_64.egg/numpy/core/_methods.py in _all(a, axis, dtype, out, keepdims, where)
62 # Parsing keyword arguments is currently fairly slow, so avoid it for now
63 if where is True:
---> 64 return umr_all(a, axis, dtype, out, keepdims)
65 return umr_all(a, axis, dtype, out, keepdims, where=where)
66
KeyboardInterrupt:
From the above plot we observe a number of differences between curves displaying positive cooperativity (\(n>1\)) and negative cooperativity ($n<1).
Negative cooperativity displays a more rapid initial increase in \(v_0\) as compared to MM or positive cooperative behavior
The rate of all value is equivalent at \([S]_0 = 1\) \(\mu\)M.
The rate of negative cooperative enzymes is lower than MM or positive cooperative behvaior for \([S]_0 > 1\)
Postive cooperative and MM curves all approach \(v_{max}\) asymptotically.
Negative cooperative curves do not approach \(v_{max}\) asymptotically.
4.4.8.7. Fitting the Hill Equation#
When fitting the Hill equation, we must consider the Michaelis-Menten like parameters (\(v_{max}\) and \(K_m\)) as well as the Hill number, \(n\). To fit these values, we have options:
Use non-linear fitting of the equation
Linearize in a Lineweaver-Burk style equation
Consider the second case, we must write the reciprocal Hill equation
Observe that this equation suggests that \(\frac{1}{v_0}\) vs \(\frac{1}{[S]_0^n}\) will be linear with slope \(\frac{K_m}{v_{max}}\) and intercept \(\frac{1}{v_{max}}\). To use this equation in a linear least-squares fitting procedure, we will need to plot \(\frac{1}{v_0}\) vs \(\frac{1}{[S]_0}\), \(\frac{1}{v_0}\) vs \(\frac{1}{[S]_0^2}\), \(\frac{1}{v_0}\) vs \(\frac{1}{[S]_0^3}\), \(...\), fit lines to each one and determine the best fit by comparing \(R^2\) values. This is not ideal, and, additionally, \(n\) can take on non-integer values. We see that non-linear fitting is the appropriate way to go here.
Let’s see how it is done.
4.4.8.7.1. Example: Fitting the Hill Equation for \(n=1\)#
In this example, we will use the same data as the example from the Michaelis-Menten notes. Thus, we expect to get a fit with \(n\approx1\). The data is as follows
\([S]_0\) (mM) |
\(v_0\) (\(\mu\)M/s) |
---|---|
1 |
2.5 |
2 |
4.0 |
5 |
6.3 |
10 |
7.6 |
20 |
9.0 |
# Put the datat into numpy arrays
s0 = np.array([1,2,5,10,20.0])
v0 = np.array([2.5,4.0,6.3,7.6,9.0])
Perform the non-linear fit and get the parameters:
Show code cell source
# perform non-linear fit
# import least squares function from scipy library
from scipy.optimize import least_squares
# define Michaelis-Menten function
def hill(x,s):
return x[0]*s**x[2]/(x[1] + s**x[2])
# define residual function (difference between function and data)
def loss(x,s,data):
return hill(x,s) - data
# make an initial guess of parameters
x0 = np.array([1.0,1.0,1.0])
res_lsq = least_squares(loss, x0, args=(s0, v0))
print("v_max = ", np.round(res_lsq.x[0],1), "micro M/s")
print("Km = ", np.round(res_lsq.x[1],1), "mM")
print("n = ", np.round(res_lsq.x[2],1), "unitless")
v_max = 10.8 micro M/s
Km = 3.2 mM
n = 0.9 unitless
Show code cell source
# plot data
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
# setup plot parameters
fontsize=16
fig = plt.figure(figsize=(8,8), dpi= 80, facecolor='w', edgecolor='k')
ax = plt.subplot(111)
ax.grid(which='major', axis='both', color='#808080', linestyle='--')
ax.set_ylabel("$v_0$ ($\mu$M/s)",size=fontsize)
ax.set_xlabel("$[S]_0$ (mM)",size=fontsize)
plt.tick_params(axis='both',labelsize=fontsize)
ax.plot(s0,v0,'o',lw=2)
s = np.arange(np.amin(s0),np.amax(s0),0.01)
ax.plot(s,hill(res_lsq.x,s),lw=3,label="fit")
plt.legend(fontsize=fontsize);
4.4.8.7.2. Example: Fitting the Hill Equation for \(n\) Different than 1#
Consider the following data, fit the Hill equation
\([S]_0\) (mM) |
\(v_0\) (\(\mu\)M/s) |
---|---|
0.05 |
0.10 |
1 |
1.26 |
2 |
4.55 |
5 |
7.18 |
10 |
7.52 |
20 |
7.42 |
# Put the data into numpy arrays
import numpy as np
s0 = np.array([0.5,1,2,5,10,20.0])
v0 = np.array([0.19,1.26,4.55,7.18,7.52,7.42])
Show code cell source
# plot data
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
# setup plot parameters
fontsize=16
fig = plt.figure(figsize=(8,8), dpi= 80, facecolor='w', edgecolor='k')
ax = plt.subplot(111)
ax.grid(which='major', axis='both', color='#808080', linestyle='--')
ax.set_ylabel("$v_0$ ($\mu$M/s)",size=fontsize)
ax.set_xlabel("$[S]_0$ (mM)",size=fontsize)
plt.tick_params(axis='both',labelsize=fontsize)
ax.plot(s0,v0,'o',lw=2);
Show code cell source
# plot data
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
# setup plot parameters
fontsize=16
fig = plt.figure(figsize=(8,8), dpi= 80, facecolor='w', edgecolor='k')
ax = plt.subplot(111)
ax.grid(which='major', axis='both', color='#808080', linestyle='--')
ax.set_ylabel("$v_0$ ($\mu$M/s)",size=fontsize)
ax.set_xlabel("$[S]_0$ (mM)",size=fontsize)
plt.tick_params(axis='both',labelsize=fontsize)
ax.plot(s0,v0,'o',lw=2)
s = np.arange(np.amin(s0),np.amax(s0),0.01)
# define Michaelis-Menten function
def hill(s,vmax,Km,n):
return vmax*s**n/(Km + s**n)
plt.plot(s,hill(s,7.5,5,1),lw=2,label="n=1")
plt.plot(s,hill(s,7.5,5,2),lw=2,label="n=2")
plt.plot(s,hill(s,7.5,5,3),lw=2,label="n=3")
plt.plot(s,hill(s,7.5,5,4),lw=2,label="n=4")
plt.legend(fontsize=fontsize);
Perform the non-linear fit to the Hill equation and get the parameters:
Show code cell source
# perform non-linear fit
# import least squares function from scipy library
from scipy.optimize import curve_fit
# define Michaelis-Menten function
def hill(s,vmax,Km,n):
return vmax*s**n/(Km + s**n)
# make an initial guess of parameters
x0 = np.array([1.0,1.0])
popt, pcov = curve_fit(hill, s0, v0)
err = np.sqrt(np.diag(pcov))
print("v_max = ", np.round(popt[0],2),"+/-", np.round(err[0],2), "muM/s")
print("Km = ", np.round(popt[1],1),"+/-", np.round(err[1],1), "mM")
print("n = ", np.round(popt[2],1),"+/-", np.round(err[2],1))
v_max = 7.49 +/- 0.04 muM/s
Km = 5.0 +/- 0.2 mM
n = 2.9 +/- 0.1
Perform a non-linear fit the MM equation for reference:
Show code cell source
# perform non-linear fit
# import least squares function from scipy library
from scipy.optimize import curve_fit
# define Michaelis-Menten function
def mm(s,vmax,Km):
return vmax*s/(Km + s)
# make an initial guess of parameters
x0 = np.array([1.0,1.0])
popt_mm, pcov = curve_fit(mm, s0, v0)
err_mm = np.sqrt(np.diag(pcov))
print("v_max = ", np.round(popt_mm[0],1),"+/-", np.round(err_mm[0],1), "muM/s")
print("Km = ", np.round(popt_mm[1],1),"+/-", np.round(err_mm[1],1), "mM")
v_max = 9.4 +/- 1.5 muM/s
Km = 2.8 +/- 1.4 mM
Plot the results:
Show code cell source
# plot data
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
# setup plot parameters
fontsize=16
fig = plt.figure(figsize=(8,8), dpi= 80, facecolor='w', edgecolor='k')
ax = plt.subplot(111)
ax.grid(which='major', axis='both', color='#808080', linestyle='--')
ax.set_ylabel("$v_0$ ($\mu$M/s)",size=fontsize)
ax.set_xlabel("$[S]_0$ (mM)",size=fontsize)
plt.tick_params(axis='both',labelsize=fontsize)
ax.plot(s0,v0,'o',lw=2)
s = np.arange(0.5,20,0.01)
ax.plot(s,hill(s,popt[0],popt[1],popt[2]),lw=2,label="Hill")
ax.plot(s,mm(s,popt_mm[0],popt_mm[1]),lw=2,label="Michaelis-Menten")
plt.legend(fontsize=fontsize);
<matplotlib.legend.Legend at 0x7fd400bc9eb0>
4.4.8.8. Example: Multiple Data Sets#
Show code cell source
from tabulate import tabulate
import numpy as np
s0 = np.array([1,2,5,10,20.0])
# Generate a data set
def hill(S0,vmax,Km,n):
return vmax*S0**n/(Km + S0**n)
vmax = 7.5
Km = 4.0
n = 0.5
truth = hill(s0,vmax,Km,n)
n_trials = 1
data = np.empty((truth.shape[0],n_trials))
s0_total = np.empty((s0.shape[0],n_trials))
for i in range(n_trials):
# estimate error based on normal distribution 99.9% data within 7.5%
error = np.random.normal(0,0.03,truth.shape[0])
# estimate error from uniform distribution with maximum value of 5%
#error = 0.1*(np.random.rand(truth.shape[0])-0.5)
# generate data by adding error to truth
data[:,i] = truth*(1+error)
# keep flattened s0 array
s0_total[:,i] = s0
combined_data = np.column_stack((s0,data))
print(tabulate(combined_data,headers=["[S]0","Trial 1", "Trial 2", "Trial 3", "Trial 4", "Trial 5"]))
[S]0 Trial 1
------ ---------
1 1.5454
2 2.0766
5 2.75409
10 3.23132
20 3.74453
# perform non-linear fit
# import least squares function from scipy library
from scipy.optimize import curve_fit
# define Michaelis-Menten function
def hill(s,vmax,Km,n):
return vmax*s**n/(Km + s**n)
# make an initial guess of parameters
popt, pcov = curve_fit(hill, s0_total.flatten(), data.flatten())
err = np.sqrt(np.diag(pcov))
print("v_max = ", np.round(popt[0],1),"+/-", np.round(err[0],1), "muM/s")
print("Km = ", np.round(popt[1],1),"+/-", np.round(err[1],1), "mM")
print("n = ", np.round(popt[2],1),"+/-", np.round(err[2],1))
v_max = 5.2 +/- 0.4 muM/s
Km = 2.3 +/- 0.2 mM
n = 0.6 +/- 0.1