E + S <-> ES (with kinetic pars k1_forward and k1_reverse), and ES -> E + P (k2_forward)¶enzyme_1_a, we were given k1_forward, k1_reverse and k2_forward... But what to do if we're just given kM and kcat¶In Part 1, we'll "cheat" and use the actual value of k1_forward
In Part 2, we'll explore what happens if, lacking an actual value, we under-estimate k1_forward
In Part 3, we'll explore what happens if we over-estimate k1_forward
Background: please see experiment enzyme_1_a
the enzyme Adenosinedeaminase with the substrate 2,6-Diamino-9-β-D-deoxyribofuranosyl-9-H-purine,
and the initial concentration values choosen below, all satisfy the customary Michaelis-Menten assumptions that
[E] << [S] and that the reaction rate constants satisfy k1_reverse >> k2_forward
Source of kinetic parameters: page 16 of "Analysis of Enzyme Reaction Kinetics, Vol. 1", by F. Xavier Malcata, Wiley, 2023
LAST_REVISED = "Nov. 4, 2024"
LIFE123_VERSION = "1.0.0.rc.0" # Library version this experiment is based on
#import set_path # Using MyBinder? Uncomment this before running the next cell!
#import sys
#sys.path.append("C:/some_path/my_env_or_install") # CHANGE to the folder containing your venv or libraries installation!
# NOTE: If any of the imports below can't find a module, uncomment the lines above, or try: import set_path
import ipynbname
import pandas as pd
from life123 import check_version, ChemData, UniformCompartment, ReactionEnz, GraphicLog, PlotlyHelper
check_version(LIFE123_VERSION)
OK
What values of k1_forward, k1_reverse and k2_forward are compatible with them?
# The following values are taken from experiment `enzyme_1`
kM = 8.27777777777777
kcat = 49
# k2_forward equals kcat ; not much to say here
k2_forward = kcat
By contrast kM = (k2_forward + k1_reverse) / k1_forward
We are given kM and k2_forward, which is the same value as the given kcat.
But how to solve for k1_forward and k1_reverse?? We have just 1 equation and 2 variables! The system of equations is "underdetermined" : what can we do?
We'll venture some guesses for k1_forward, and compute the corresponding k1_reverse that will satisfy the above equation.
k1_reverse = kM * k1_forward - kcat (kcat has same value as k2_forward)
# Example, using the k1_forward=18. from experiment `enzyme_1`
k1_forward = 18.
k1_reverse = kM * k1_forward - kcat
k1_reverse
99.99999999999986
Naturally, we're getting the same value we had for k1_reverse in that experiment. But what if k1_forward is quite different?
import numpy as np
def compute_k1_reverse(kM, kcat, k1_forward, verbose=True):
k1_reverse = kM * k1_forward - kcat
if verbose:
if np.allclose(k1_reverse, 0):
print (f"k1_reverse: {k1_reverse} , K = INFINITE")
else:
K = k1_forward / k1_reverse
print(f"k1_reverse: {k1_reverse} , K = {K}")
return k1_reverse
compute_k1_reverse(kM, kcat, k1_forward=18.)
k1_reverse: 99.99999999999986 , K = 0.18000000000000024
99.99999999999986
compute_k1_reverse(kM, kcat, k1_forward=0.1)
k1_reverse: -48.172222222222224 , K = -0.002075885134355899
-48.172222222222224
kM * k1_forward - kcat > 0
i.e. k1_forward > kcat/kM
In other words, k1_forward_min = kcat/kM
k1_forward_min = kcat / kM
k1_forward_min
5.919463087248328
compute_k1_reverse(kM, kcat, k1_forward=k1_forward_min)
k1_reverse: 0.0 , K = INFINITE
0.0
compute_k1_reverse(kM, kcat, k1_forward=6.)
k1_reverse: 0.6666666666666146 , K = 9.000000000000703
0.6666666666666146
compute_k1_reverse(kM, kcat, k1_forward=10.)
k1_reverse: 33.7777777777777 , K = 0.29605263157894807
33.7777777777777
compute_k1_reverse(kM, kcat, k1_forward=40.)
k1_reverse: 282.1111111111108 , K = 0.14178810555336763
282.1111111111108
The fixed point k1_forward = k1_reverse occurs when k1_forward = kM * k1_forward - kcat , i.e.
kM * k1_forward - k1_forward = kcat
k1_forward * (kM - 1) = kcat
k1_forward = kcat / (kM - 1)
Notice that the above makes sense only if kM > 1
# Our kM is indeed > 1, so all good:
k1_forward_fixed_pnt = kcat / (kM - 1)
k1_forward_fixed_pnt
6.7328244274809235
compute_k1_reverse(kM, kcat, k1_forward=6.7328244274809235)
k1_reverse: 6.732824427480921 , K = 1.0000000000000004
6.732824427480921
Indeed, a fixed point
k1_forward_choices = np.linspace(5.92, 40., 2000) # Grid creation
k1_reverse_choices = compute_k1_reverse(kM, kcat, k1_forward=k1_forward_choices, verbose=False)
import plotly.express as px
px.line(x=k1_forward_choices, y=k1_reverse_choices, title="k1_forward vs. k1_reverse")
K_choices = k1_forward_choices / k1_reverse_choices
px.line(x=k1_forward_choices, y=np.log(K_choices), title="log(K) vs. k1_forward")
S0 = 20.
E0 = 1.
k1_forward and k1_reverse¶Source: page 16 of "Analysis of Enzyme Reaction Kinetics, Vol. 1", by F. Xavier Malcata, Wiley, 2023
k1_forward = 18.
k1_reverse = 100.
chem_data = ChemData(names=["P", "ES"])
# Our Enzyme
chem_data.add_chemical(name="Adenosinedeaminase", label="E")
# Our Substrate
chem_data.add_chemical(name="2,6-Diamino-9-β-D-deoxyribofuranosyl-9-H-purine", label="S");
chem_data.all_chemicals()
| name | label | |
|---|---|---|
| 0 | P | P |
| 1 | ES | ES |
| 2 | Adenosinedeaminase | E |
| 3 | 2,6-Diamino-9-β-D-deoxyribofuranosyl-9-H-purine | S |
# Elementary Reaction E + S <-> ES
chem_data.add_reaction(reactants=["E", "S"], products=["ES"],
forward_rate=k1_forward, reverse_rate=k1_reverse)
# Elementary Reaction ES <-> E + P
chem_data.add_reaction(reactants=["ES"], products=["E", "P"],
forward_rate=kcat, reverse_rate=0)
chem_data.describe_reactions()
Number of reactions: 2 (at temp. 25 C)
0: E + S <-> ES (kF = 18 / kR = 100 / delta_G = 4,250.9 / K = 0.18) | 1st order in all reactants & products
1: ES <-> E + P (kF = 49 / kR = 0) | 1st order in all reactants & products
Set of chemicals involved in the above reactions: {'E', 'S', 'P', 'ES'}
# Here we use the "slower" preset for the variable steps, a conservative option prioritizing accuracy over speed
uc = UniformCompartment(chem_data=chem_data, preset="slower")
uc.set_conc(conc={"S": S0, "E": E0}) # Small ampount of enzyme `E`, relative to substrate `S`
uc.describe_state()
SYSTEM STATE at Time t = 0:
4 species:
Species 0 (P). Conc: 0.0
Species 1 (ES). Conc: 0.0
Species 2 (E). Conc: 1.0
Species 3 (S). Conc: 20.0
Set of chemicals involved in reactions: {'E', 'S', 'P', 'ES'}
uc.enable_diagnostics() # To save diagnostic information about the simulation - in particular, the REACTION RATES
# Perform the reactions
uc.single_compartment_react(duration=1.2, initial_step=0.05)
Some steps were backtracked and re-done, to prevent negative concentrations or excessively large concentration changes
784 total step(s) taken
Number of step re-do's because of negative concentrations: 2
Number of step re-do's because of elective soft aborts: 1
Norm usage: {'norm_A': 706, 'norm_B': 710, 'norm_C': 706, 'norm_D': 706}
uc.plot_history(colors=['cyan', 'green', 'violet', 'red'], show_intervals=True,
title_prefix="Using exact values for k1_forward and k1_reverse")
plot_pandas() NOTICE: Excessive number of vertical lines (785) - only showing 1 every 6 lines
P?¶One could take the numerical derivative (gradient) of the time values of [P] - but no need to! Reaction rates are computed in the course of the simulation, and stored alongside other diagnostic data, provided that diagnostics were enabled (as we did indeed enable)
history_with_rates_exact = uc.get_diagnostics().get_system_history_with_rxn_rates(rxn_index=1) # We specify the reaction that involves `P`
history_with_rates_exact
Reaction: ES <-> E + P
| TIME | P | ES | E | S | caption | rate | |
|---|---|---|---|---|---|---|---|
| 0 | 0.000000 | 0.000000 | 0.000000 | 1.000000 | 20.000000 | 0.000000 | |
| 1 | 0.000500 | 0.000000 | 0.180000 | 0.820000 | 19.820000 | 8.820000 | |
| 2 | 0.000750 | 0.002205 | 0.246431 | 0.753569 | 19.751364 | 12.075109 | |
| 3 | 0.000763 | 0.002356 | 0.249321 | 0.750679 | 19.748323 | 12.216716 | |
| 4 | 0.000769 | 0.002432 | 0.250756 | 0.749244 | 19.746811 | 12.287060 | |
| ... | ... | ... | ... | ... | ... | ... | ... |
| 779 | 1.166644 | 19.724207 | 0.029857 | 0.970143 | 0.245936 | 1.462999 | |
| 780 | 1.173671 | 19.734488 | 0.028775 | 0.971225 | 0.236738 | 1.409959 | |
| 781 | 1.180698 | 19.744395 | 0.027729 | 0.972271 | 0.227875 | 1.358736 | |
| 782 | 1.187725 | 19.753943 | 0.026720 | 0.973280 | 0.219337 | 1.309274 | |
| 783 | 1.194752 | 19.763144 | 0.025745 | 0.974255 | 0.211111 | 1.261520 |
784 rows × 7 columns
# Let's take a look at how the reaction rate varies with time
PlotlyHelper.plot_pandas(df=history_with_rates_exact,
title="Reaction rate, dP/dt, over time",
x_var="TIME", fields="rate",
x_label="time", y_label="dP/dt")
k1_forward (UNDER-ESTIMATED)¶chem_data = ChemData(names=["P", "ES"])
# Our Enzyme
chem_data.add_chemical(name="Adenosinedeaminase", label="E")
# Our Substrate
chem_data.add_chemical(name="2,6-Diamino-9-β-D-deoxyribofuranosyl-9-H-purine", label="S");
k1_forward = 6.5 # Close to the smallest possible value
k1_reverse = compute_k1_reverse(kM, kcat, k1_forward=k1_forward)
k1_reverse
k1_reverse: 4.8055555555555 , K = 1.3526011560693798
4.8055555555555
# Reaction E + S <-> ES , with 1st-order kinetics,
# and a forward rate that is much faster than its revers one
chem_data.add_reaction(reactants=["E", "S"], products=["ES"],
forward_rate=k1_forward, reverse_rate=k1_reverse)
# Reaction ES <-> E + P , with 1st-order kinetics
chem_data.add_reaction(reactants=["ES"], products=["E", "P"],
forward_rate=49.,reverse_rate=0)
chem_data.describe_reactions()
Number of reactions: 2 (at temp. 25 C)
0: E + S <-> ES (kF = 6.5 / kR = 4.8056 / delta_G = -748.72 / K = 1.3526) | 1st order in all reactants & products
1: ES <-> E + P (kF = 49 / kR = 0) | 1st order in all reactants & products
Set of chemicals involved in the above reactions: {'E', 'S', 'P', 'ES'}
# Here we use the "slower" preset for the variable steps, a conservative option prioritizing accuracy over speed
uc = UniformCompartment(chem_data=chem_data, preset="slower")
uc.set_conc(conc={"S": S0, "E": E0}) # Small ampount of enzyme `E`, relative to substrate `S`
uc.describe_state()
SYSTEM STATE at Time t = 0:
4 species:
Species 0 (P). Conc: 0.0
Species 1 (ES). Conc: 0.0
Species 2 (E). Conc: 1.0
Species 3 (S). Conc: 20.0
Set of chemicals involved in reactions: {'E', 'S', 'P', 'ES'}
uc.enable_diagnostics() # To save diagnostic information about the simulation - in particular, the REACTION RATES
# Perform the reactions
uc.single_compartment_react(duration=1.2, initial_step=0.05)
Some steps were backtracked and re-done, to prevent negative concentrations or excessively large concentration changes
788 total step(s) taken
Number of step re-do's because of negative concentrations: 1
Number of step re-do's because of elective soft aborts: 2
Norm usage: {'norm_A': 704, 'norm_B': 709, 'norm_C': 704, 'norm_D': 704}
uc.plot_history(colors=['cyan', 'green', 'violet', 'red'], show_intervals=True,
title_prefix="Using smaller value for k1_forward")
plot_pandas() NOTICE: Excessive number of vertical lines (789) - only showing 1 every 6 lines
At first glance, not too different from before, overall, but let's take a closer look
history_with_rates_under = uc.get_diagnostics().get_system_history_with_rxn_rates(rxn_index=1) # We specify the reaction that involves `P`
history_with_rates_under
Reaction: ES <-> E + P
| TIME | P | ES | E | S | caption | rate | |
|---|---|---|---|---|---|---|---|
| 0 | 0.000000 | 0.000000 | 0.000000 | 1.000000 | 20.000000 | 0.000000 | |
| 1 | 0.000500 | 0.000000 | 0.065000 | 0.935000 | 19.935000 | 3.185000 | |
| 2 | 0.000750 | 0.000796 | 0.094414 | 0.905586 | 19.904789 | 4.626306 | |
| 3 | 0.000763 | 0.000854 | 0.095815 | 0.904185 | 19.903330 | 4.694958 | |
| 4 | 0.000769 | 0.000883 | 0.096514 | 0.903486 | 19.902602 | 4.729203 | |
| ... | ... | ... | ... | ... | ... | ... | ... |
| 783 | 1.171227 | 19.767701 | 0.026902 | 0.973098 | 0.205396 | 1.318201 | |
| 784 | 1.178115 | 19.776782 | 0.025880 | 0.974120 | 0.197338 | 1.268138 | |
| 785 | 1.185004 | 19.785518 | 0.024895 | 0.975105 | 0.189587 | 1.219867 | |
| 786 | 1.191892 | 19.793921 | 0.023946 | 0.976054 | 0.182134 | 1.173332 | |
| 787 | 1.198781 | 19.802003 | 0.023030 | 0.976970 | 0.174967 | 1.128479 |
788 rows × 7 columns
# Let's take a look at how the reaction rate varies with time
PlotlyHelper.plot_pandas(df=history_with_rates_under,
title="Reaction rate, dP/dt, over time",
x_var="TIME", fields="rate",
x_label="time", y_label="dP/dt")
k1_forward (OVER-ESTIMATED)¶chem_data = ChemData(names=["P", "ES"])
# Our Enzyme
chem_data.add_chemical(name="Adenosinedeaminase", label="E")
# Our Substrate
chem_data.add_chemical(name="2,6-Diamino-9-β-D-deoxyribofuranosyl-9-H-purine", label="S");
k1_forward = 40. # Well above the known value of 18.
k1_reverse = compute_k1_reverse(kM, kcat, k1_forward=k1_forward)
k1_reverse
k1_reverse: 282.1111111111108 , K = 0.14178810555336763
282.1111111111108
# Reaction E + S <-> ES , with 1st-order kinetics,
# and a forward rate that is much faster than its revers one
chem_data.add_reaction(reactants=["E", "S"], products=["ES"],
forward_rate=k1_forward, reverse_rate=k1_reverse)
# Reaction ES <-> E + P , with 1st-order kinetics
chem_data.add_reaction(reactants=["ES"], products=["E", "P"],
forward_rate=49.,reverse_rate=0)
chem_data.describe_reactions()
Number of reactions: 2 (at temp. 25 C)
0: E + S <-> ES (kF = 40 / kR = 282.11 / delta_G = 4,842.4 / K = 0.14179) | 1st order in all reactants & products
1: ES <-> E + P (kF = 49 / kR = 0) | 1st order in all reactants & products
Set of chemicals involved in the above reactions: {'E', 'S', 'P', 'ES'}
# Here we use the "slower" preset for the variable steps, a conservative option prioritizing accuracy over speed
uc = UniformCompartment(chem_data=chem_data, preset="slower")
uc.set_conc(conc={"S": S0, "E": E0}) # Small ampount of enzyme `E`, relative to substrate `S`
uc.describe_state()
SYSTEM STATE at Time t = 0:
4 species:
Species 0 (P). Conc: 0.0
Species 1 (ES). Conc: 0.0
Species 2 (E). Conc: 1.0
Species 3 (S). Conc: 20.0
Set of chemicals involved in reactions: {'E', 'S', 'P', 'ES'}
uc.enable_diagnostics() # To save diagnostic information about the simulation - in particular, the REACTION RATES
# Perform the reactions
uc.single_compartment_react(duration=1.2, initial_step=0.05)
Some steps were backtracked and re-done, to prevent negative concentrations or excessively large concentration changes
864 total step(s) taken
Number of step re-do's because of negative concentrations: 2
Number of step re-do's because of elective soft aborts: 1
Norm usage: {'norm_A': 829, 'norm_B': 835, 'norm_C': 829, 'norm_D': 829}
uc.plot_history(colors=['cyan', 'green', 'violet', 'red'], show_intervals=True,
title_prefix="Using smaller value for k1_forward")
plot_pandas() NOTICE: Excessive number of vertical lines (865) - only showing 1 every 6 lines
At first glance, not too different from before, overall, but let's take a closer look
history_with_rates_over = uc.get_diagnostics().get_system_history_with_rxn_rates(rxn_index=1) # We specify the reaction that involves `P`
history_with_rates_over
Reaction: ES <-> E + P
| TIME | P | ES | E | S | caption | rate | |
|---|---|---|---|---|---|---|---|
| 0 | 0.000000 | 0.000000 | 0.000000 | 1.000000 | 20.000000 | 0.000000 | |
| 1 | 0.000500 | 0.000000 | 0.400000 | 0.600000 | 19.600000 | 19.600000 | |
| 2 | 0.000750 | 0.004900 | 0.484489 | 0.515511 | 19.510611 | 23.739956 | |
| 3 | 0.000763 | 0.005197 | 0.487513 | 0.512487 | 19.507291 | 23.888118 | |
| 4 | 0.000769 | 0.005346 | 0.489003 | 0.510997 | 19.505651 | 23.961149 | |
| ... | ... | ... | ... | ... | ... | ... | ... |
| 859 | 1.170211 | 19.713808 | 0.030426 | 0.969574 | 0.255766 | 1.490884 | |
| 860 | 1.176184 | 19.722714 | 0.029500 | 0.970500 | 0.247787 | 1.445487 | |
| 861 | 1.182158 | 19.731349 | 0.028612 | 0.971388 | 0.240039 | 1.401986 | |
| 862 | 1.188131 | 19.739724 | 0.027735 | 0.972265 | 0.232542 | 1.358993 | |
| 863 | 1.194105 | 19.747842 | 0.026901 | 0.973099 | 0.225257 | 1.318155 |
864 rows × 7 columns
# Let's take a look at how the reaction rate varies with time
PlotlyHelper.plot_pandas(df=history_with_rates_over,
title="Reaction rate, dP/dt, over time",
x_var="TIME", fields="rate",
x_label="time", y_label="dP/dt")
ES over time¶es_under = PlotlyHelper.plot_pandas(df=history_with_rates_under,
title="underestimated k1_forward",
x_var="TIME", fields="ES",
x_label="time", y_label="ES", colors = "yellow")
es_exact = PlotlyHelper.plot_pandas(df=history_with_rates_exact,
title="exact k1_forward",
x_var="TIME", fields="ES",
x_label="time", y_label="ES", colors = "green")
es_over = PlotlyHelper.plot_pandas(df=history_with_rates_over,
title="overestimated k1_forward",
x_var="TIME", fields="ES",
x_label="time", y_label="ES", colors = "purple")
PlotlyHelper.combine_plots([es_under, es_exact, es_over],
title="ES")
P over time¶p_under = PlotlyHelper.plot_pandas(df=history_with_rates_under,
title="underestimated k1_forward",
x_var="TIME", fields="P",
x_label="time", y_label="P", colors = "yellow")
p_exact = PlotlyHelper.plot_pandas(df=history_with_rates_exact,
title="exact k1_forward",
x_var="TIME", fields="P",
x_label="time", y_label="P", colors = "cyan")
p_over = PlotlyHelper.plot_pandas(df=history_with_rates_over,
title="overestimated k1_forward",
x_var="TIME", fields="P",
x_label="time", y_label="P", colors = "purple")
PlotlyHelper.combine_plots([p_under, p_exact, p_over],
title="P")
r1_under = PlotlyHelper.plot_pandas(df=history_with_rates_under,
title="underestimated k1_forward",
x_var="TIME", fields="rate",
x_label="time", y_label="dP/dt", colors = "yellow")
r1_exact = PlotlyHelper.plot_pandas(df=history_with_rates_exact,
title="exact k1_forward",
x_var="TIME", fields="rate",
x_label="time", y_label="dP/dt", colors = "blue")
r1_over = PlotlyHelper.plot_pandas(df=history_with_rates_over,
title="overestimated k1_forward",
x_var="TIME", fields="rate",
x_label="time", y_label="dP/dt", colors = "purple")
PlotlyHelper.combine_plots([r1_under, r1_exact, r1_over],
title="Reaction Rate")