#!/usr/bin/env python # coding: utf-8 # ## Enzyme Kinetics in a NON Michaelis-Menten modality # #### 3 Coupled Reactions: `S <-> P` , `E + S <-> ES`, and `ES <-> E + P` # A direct reaction and the same reaction, catalyzed by an enzyme `E` and showing the intermediate state. # Re-run from same initial concentrations of S ("Substrate") and P ("Product"), for various concentations of the enzyme `E`: from zero to hugely abundant # ### We'll REJECT the customary Michaelis-Menten assumptions that `[E] << [S]` and that the rate constants satisfy `k1_reverse >> k2_forward` ! # #### We'll **repeat runs with increasingly higher initial concentration of Enzyme**, and explore exotic scenarios with lavish amount of enzyme, leading to diminishing (though fast-produced!) products, and a buildup of the (not-so-transient!) `ES` intermediate # ### TAGS : "uniform compartment", "chemistry", "enzymes" # In[1]: LAST_REVISED = "Dec. 15, 2024" LIFE123_VERSION = "1.0-rc.1" # Library version this experiment is based on # In[2]: #import set_path # Using MyBinder? Uncomment this before running the next cell! # In[3]: #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 from experiments.get_notebook_info import get_notebook_basename from life123 import check_version, ChemData, Reactions, UniformCompartment, CollectionTabular, GraphicLog import pandas as pd # In[4]: check_version(LIFE123_VERSION) # In[ ]: # In[5]: # Initialize the HTML logging log_file = get_notebook_basename() + ".log.htm" # Use the notebook base filename for the log file # Set up the use of some specified graphic (Vue) components GraphicLog.config(filename=log_file, components=["vue_cytoscape_2"], extra_js="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.21.2/cytoscape.umd.js") # In[6]: # Initialize the system chem_data = ChemData(names=["S","E","ES","P"], plot_colors=["cyan","violet","red","green"]) chem_data.all_chemicals() # In[7]: rxns = Reactions(chem_data=chem_data) # Reaction S <-> P , with 1st-order kinetics, favorable thermodynamics in the forward direction, # and a forward rate that is much slower than it would be with the enzyme - as seen in the next reaction, below rxns.add_reaction(reactants="S", products="P", forward_rate=1., delta_G=-3989.73) # Reaction E + S <-> ES , with 1st-order kinetics, and a forward rate that is much faster than it was without the enzyme # Thermodynamically, the forward direction is at a disadvantage (higher energy state) because of the activation barrier in forming the transient state ES rxns.add_reaction(reactants=["E", "S"], products="ES", forward_rate=100., delta_G=2000) # Reaction ES <-> E + P , with 1st-order kinetics, and a forward rate that is much faster than it was without the enzyme # Thermodynamically, the total energy change of this reaction and the previous one adds up to the same value as the reaction without the enzyme (-3989.73) rxns.add_reaction(reactants="ES", products=["E", "P"], forward_rate=200., delta_G=-5989.73) rxns.describe_reactions() # Send the plot of the reaction network to the HTML log file rxns.plot_reaction_network("vue_cytoscape_2") # Note that `E` is not labeled as an "enzyme" because it doesn't appear as a catalyst in any of the registered reactions; it only becomes an enzyme in the context of the _compound_ reaction from (2) and (3) # In[ ]: # In[ ]: # # 1. Set the initial concentrations of all the chemicals - starting with no enzyme # In[8]: uc = UniformCompartment(reactions=rxns, preset="mid") uc.set_conc(conc={"S": 20.}) # Initially, no enzyme `E` uc.describe_state() # ### Advance the reactions (for now without enzyme) to equilibrium # In[9]: #uc.set_diagnostics() # To save diagnostic information about the call to single_compartment_react() # Perform the reactions uc.single_compartment_react(duration=4.0, initial_step=0.1) # In[10]: uc.plot_history(show_intervals=True, title_prefix="With ZERO enzyme") # ### The reactions, lacking enzyme, are proceeding slowly towards equilibrium, just like the reaction that was discussed in part 1 of the experiment "enzyme_1" # In[11]: # Verify that the reactions have reached equilibrium uc.is_in_equilibrium(tolerance=2) # In[12]: # Locate the intersection of the curves for [S] and [P]: uc.curve_intersect("S", "P", t_start=0, t_end=1.0) # In[13]: uc.get_history(columns=['SYSTEM TIME', 'E', 'ES', 'P'], tail=1) # Last point in the simulation # In[14]: P_equil = uc.get_chem_conc("P") P_equil # In[15]: P_70_threshold = P_equil * 0.70 P_70_threshold # In[16]: uc.reach_threshold(chem="P", threshold=P_70_threshold) # In[ ]: # In[ ]: # # 2. Re-start all the reactions from the same initial concentrations - except for now having a tiny amount of enzyme (two orders of magnitude less than the starting [S]) # In[17]: E_init = 0.2 # A tiny bit of enzyme `E`: 1/100 of the initial [S] # In[18]: uc = UniformCompartment(reactions=rxns, preset="slower") # A brand-new simulation, with the same chemicals and reactions as before uc.set_conc(conc={"S": 20., "E": E_init}) uc.describe_state() # In[19]: # Perform the reactions (The duration of the run was manually adjusted for optimal visibility) uc.single_compartment_react(duration=1.3, initial_step=0.00005) # In[20]: # Verify that the reactions have reached equilibrium uc.is_in_equilibrium(verbose=False) # In[21]: # Locate the intersection of the curves for [S] and [P]: uc.curve_intersect("S", "P", t_start=0, t_end=1.0) # In[22]: # Early part of the reaction uc.plot_history(title_prefix=f"Early times when E0 = {E_init}", range_x=[0, 0.4]) # In[23]: # Locate the intersection of the curves for [S] and [P]: uc.curve_intersect("S", "P", t_start=0, t_end=0.4) # ### Now, let's investigate E and ES in the very early times # ## Notice how even a tiny amount of enzyme (1/100 of the initial [S]) makes a very pronounced difference! # In[24]: # The very early part of the reaction uc.plot_history(chemicals=['E', 'ES', 'P'], title_prefix=f"Detail when E0 = {E_init}", range_x=[0, 0.002], range_y=[0, 0.2]) # ### Notice how, with this small initial concentration of [E], the timescale of [E] and [ES] is vastly faster than that of [P] and [S] # In[25]: # The full reaction of E and ES uc.plot_history(chemicals=['E', 'ES'], show_intervals=True, title_prefix=f"E and ES when E0 = {E_init}") # Notice how at every onset of instability in [E] or [ES], the adaptive time steps shrink down # In[26]: uc.get_history(columns=['SYSTEM TIME', 'E', 'ES', 'P'], tail=1) # Last point in the simulation # Interestingly, most of the inital [E] of 0.2 is now, at equilibrium, stored as [ES]=0.119; the energy of the "activation barrier" from E + S to ES might be unrealistically low (2000 Joules). Zooming in on the very earl part of the plot: # In[27]: P_equil = uc.get_chem_conc("P") P_equil # In[28]: P_70_threshold = P_equil * 0.70 P_70_threshold # In[29]: uc.reach_threshold(chem="P", threshold=P_70_threshold) # In[ ]: # In[ ]: # # 3. Keep increasing the initial [E] # In[30]: E_init = 1.0 # In[31]: uc = UniformCompartment(reactions=rxns, preset="slower") # A brand-new simulation, with the same chemicals and reactions as before uc.set_conc(conc={"S": 20., "E": E_init}) uc.describe_state() # In[32]: # Perform the reactions (The duration of the run was manually adjusted for optimal visibility) uc.single_compartment_react(duration=0.4, initial_step=0.00005) # In[33]: # Verify that the reactions have reached equilibrium uc.is_in_equilibrium(verbose=False) # In[34]: uc.plot_history(show_intervals=True, title_prefix=f"E0 = {E_init}") # In[35]: # Locate the intersection of the curves for [S] and [P]: uc.curve_intersect("S", "P", t_start=0, t_end=0.4) # The timescale of [S] and [P] continues to become faster with a higher initial [E] # In[36]: # The very early part of the reactions uc.plot_history(chemicals=['E', 'ES', 'P'], title_prefix=f"Detail when E0 = {E_init}", range_x=[0, 0.002], range_y=[0, 1.]) # In[37]: # The full reaction of E and ES uc.plot_history(chemicals=['E', 'ES'], show_intervals=True, title_prefix=f"E and ES when E0 = {E_init}") # In[38]: uc.get_history(columns=['SYSTEM TIME', 'E', 'ES', 'P'], tail=1) # Last point in the simulation # In[39]: P_equil = uc.get_chem_conc("P") P_equil # #### P_equil continues to decrease with increasing initial [E] # In[40]: P_70_threshold = P_equil * 0.70 P_70_threshold # In[41]: uc.reach_threshold(chem="P", threshold=P_70_threshold) # #### The time at which we reach the 70% threshold of the equilibrium value of P continues to decrease with increasing initial [E] # In[ ]: # In[ ]: # # 4. Keep increasing the initial [E] # In[42]: E_init = 2.0 # 1/10 of the initial [S] # In[43]: uc = UniformCompartment(reactions=rxns, preset="slower") # A brand-new simulation, with the same chemicals and reactions as before uc.set_conc(conc={"S": 20., "E": E_init}) uc.describe_state() # In[44]: # Perform the reactions (The duration of the run was manually adjusted for optimal visibility) uc.single_compartment_react(duration=0.2, initial_step=0.00005) # In[45]: # Verify that the reactions have reached equilibrium uc.is_in_equilibrium(verbose=False) # In[46]: uc.plot_history(show_intervals=True, title_prefix=f"E0 = {E_init}") # In[47]: # Locate the intersection of the curves for [S] and [P]: uc.curve_intersect("S", "P", t_start=0, t_end=0.4) # The timescale of [S] and [P] continues to become faster with a higher initial [E] # In[48]: #The very early part of the reaction uc.plot_history(chemicals=['E', 'ES', 'P'], title_prefix=f"Detail when E0 = {E_init}", range_x=[0, 0.002], range_y=[0, 2.]) # In[49]: # Show the full reaction of E and ES uc.plot_history(chemicals=['E', 'ES'], show_intervals=True, title_prefix=f"E and ES when E0 = {E_init}") # In[50]: uc.get_history(columns=['SYSTEM TIME', 'E', 'ES', 'P'], tail=1) # Last point in the simulation # In[51]: P_equil = uc.get_chem_conc("P") P_equil # #### P_equil continues to decrease with increasing initial [E] # In[52]: P_70_threshold = P_equil * 0.70 P_70_threshold # In[53]: uc.reach_threshold(chem="P", threshold=P_70_threshold) # #### The time at which we reach the 70% threshold of the equilibrium value of P continues to decrease with increasing initial [E] # In[ ]: # In[ ]: # # 5. Keep increasing the initial [E] # In[54]: E_init = 10.0 # 1/2 of the initial [S] # In[55]: uc = UniformCompartment(reactions=rxns, preset="slower") # A brand-new simulation, with the same chemicals and reactions as before uc.set_conc(conc={"S": 20., "E": E_init}) uc.describe_state() # In[56]: # Perform the reactions (The duration of the run was manually adjusted for optimal visibility) uc.single_compartment_react(duration=0.05, initial_step=0.00005) # In[57]: # Verify that the reactions have reached equilibrium uc.is_in_equilibrium(verbose=False) # In[58]: uc.plot_history(show_intervals=True, title_prefix=f"E0 = {E_init}") # In[59]: # Locate the intersection of the curves for [S] and [P]: uc.curve_intersect("S", "P", t_start=0, t_end=0.05) # #### The timescale of [S] and [P] continues to become faster with a higher initial [E] -- **AND THEY'RE NOW APPROACHING THE TIMESCALES OF E AND ES** # In[60]: # The very early part of the reaction uc.plot_history(chemicals=['E', 'ES', 'P'], title_prefix=f"Detail when E0 = {E_init}", range_x=[0, 0.002], range_y=[0, 10.]) # In[61]: # The full reaction of E and ES uc.plot_history(chemicals=['E', 'ES'], show_intervals=True, title_prefix=f"E and ES when E0 = {E_init}") # #### Notice that at these higher initial concentrations of [E], we're now beginning to see overshoots in [E] and [ES] # In[62]: uc.get_history(columns=['SYSTEM TIME', 'E', 'ES', 'P'], tail=1) # Last point in the simulation # In[63]: P_equil = uc.get_chem_conc("P") P_equil # #### P_equil continues to decrease with increasing initial [E] # In[64]: P_70_threshold = P_equil * 0.70 P_70_threshold # In[65]: uc.reach_threshold(chem="P", threshold=P_70_threshold) # #### The time at which we reach the 70% threshold of the equilibrium value of P continues to decrease with increasing initial [E] # In[ ]: # In[ ]: # # 6. Keep increasing the initial [E] # In[66]: E_init = 20.0 # Same as the initial [S] # In[67]: uc = UniformCompartment(reactions=rxns, preset="slower") # A brand-new simulation, with the same chemicals and reactions as before uc.set_conc(conc={"S": 20., "E": E_init}) uc.describe_state() # In[68]: # Perform the reactions (The duration of the run was manually adjusted for optimal visibility) uc.single_compartment_react(duration=0.02, initial_step=0.00005) # In[69]: # Verify that the reactions have reached equilibrium uc.is_in_equilibrium(verbose=False) # In[70]: uc.plot_history(show_intervals=True, title_prefix=f"E0 = {E_init}") # In[71]: # Locate the intersection of the curves for [S] and [P]: uc.curve_intersect("S", "P", t_start=0, t_end=0.02) # #### The timescale of [S] and [P] continues to become faster with a higher initial [E] -- **AND THEY'RE NOW GETTING CLOSE THE TIMESCALES OF E AND ES** # In[72]: # The very early part of the reaction uc.plot_history(title_prefix=f"Detail when E0 = {E_init}", range_x=[0, 0.002], range_y=[0, 20.]) # In[73]: # The full reaction of E and ES uc.plot_history(chemicals=['E', 'ES'], show_intervals=True, title_prefix=f"E and ES when E0 = {E_init}") # #### At these higher initial concentrations of [E], we're now beginning to see overshoots in [E] and [ES] that overlap less # In[74]: uc.get_history(columns=['SYSTEM TIME', 'E', 'ES', 'P'], tail=1) # Last point in the simulation # In[75]: P_equil = uc.get_chem_conc("P") P_equil # #### P_equil continues to decrease with increasing initial [E] # In[76]: P_70_threshold = P_equil * 0.70 P_70_threshold # In[77]: uc.reach_threshold(chem="P", threshold=P_70_threshold) # #### The time at which we reach the 70% threshold of the equilibrium value of P continues to decrease with increasing initial [E] # In[ ]: # In[ ]: # # 7. Keep increasing the initial [E] # In[78]: E_init = 30.0 # 50% higher than the initial [S] # In[79]: uc = UniformCompartment(reactions=rxns, preset="slower") # A brand-new simulation, with the same chemicals and reactions as before uc.set_conc(conc={"S": 20., "E": E_init}) uc.describe_state() # In[80]: # Perform the reactions (The duration of the run was manually adjusted for optimal visibility) uc.single_compartment_react(duration=0.01, initial_step=0.00005) # In[81]: # Verify that the reactions have reached equilibrium uc.is_in_equilibrium(verbose=False) # In[82]: uc.plot_history(show_intervals=True, title_prefix=f"E0 = {E_init}") # In[83]: # Locate the intersection of the curves for [S] and [P]: uc.curve_intersect("S", "P", t_start=0, t_end=0.01) # #### The timescale of [S] and [P] continues to become faster with a higher initial [E] -- **AND THEY'RE NOW COMPARABLE THE TIMESCALES OF E AND ES** # In[84]: #The very early part of the reaction uc.plot_history(title_prefix=f"Detail when E0 = {E_init}", range_x=[0, 0.005], range_y=[0, 30.]) # In[85]: # The full reaction of E and ES uc.plot_history(chemicals=['E', 'ES'], show_intervals=True, title_prefix=f"E and ES when E0 = {E_init}") # #### At these high initial concentrations of [E], we're now beginning to see [E] and [ES] no longer overlapping; the overshoot is still present in both # In[86]: uc.get_history(columns=['SYSTEM TIME', 'E', 'ES', 'P'], tail=1) # Last point in the simulation # In[87]: P_equil = uc.get_chem_conc("P") P_equil # #### P_equil continues to decrease with increasing initial [E] # In[88]: P_70_threshold = P_equil * 0.70 P_70_threshold # In[89]: uc.reach_threshold(chem="P", threshold=P_70_threshold) # #### The time at which we reach the 70% threshold of the equilibrium value of P continues to decrease with increasing initial [E] # In[ ]: # In[ ]: # # 8. Keep increasing the initial [E] # In[90]: E_init = 60.0 # Triple the initial [S] # In[91]: uc = UniformCompartment(reactions=rxns, preset="slower") # A brand-new simulation, with the same chemicals and reactions as before uc.set_conc(conc={"S": 20., "E": E_init}) uc.describe_state() # In[92]: # Perform the reactions (The duration of the run was manually adjusted for optimal visibility) uc.single_compartment_react(duration=0.005, initial_step=0.00005) # In[93]: # Verify that the reactions have reached equilibrium uc.is_in_equilibrium(verbose=False) # In[94]: uc.plot_history(show_intervals=True, title_prefix=f"E0 = {E_init}") # In[95]: # Locate the intersection of the curves for [S] and [P]: uc.curve_intersect("S", "P", t_start=0, t_end=0.005) # #### The timescales of [S] and [P] continues to become faster with a higher initial [E] -- **AND NOW THEY REMAIN COMPARABLE THE TIMESCALES OF E AND ES** # #### At these high initial concentrations of [E], [E] and [ES] no longer overlapping, and are now getting further apart # In[96]: uc.get_history(columns=['SYSTEM TIME', 'E', 'ES', 'P'], tail=1) # Last point in the simulation # In[97]: P_equil = uc.get_chem_conc("P") P_equil # #### P_equil continues to decrease with increasing initial [E] # In[98]: P_70_threshold = P_equil * 0.70 P_70_threshold # In[99]: uc.reach_threshold(chem="P", threshold=P_70_threshold) # #### The time at which we reach the 70% threshold of the equilibrium value of P continues to decrease with increasing initial [E] # In[ ]: # In[ ]: # # 9. Keep increasing the initial [E] # In[100]: E_init = 100.0 # Quintuple the initial [S] # In[101]: uc = UniformCompartment(reactions=rxns, preset="slower") # A brand-new simulation, with the same chemicals and reactions as before uc.set_conc(conc={"S": 20., "E": E_init}) uc.describe_state() # In[102]: # Perform the reactions (The duration of the run was manually adjusted for optimal visibility) uc.single_compartment_react(duration=0.003, initial_step=0.00005) # In[103]: # Verify that the reactions have reached equilibrium uc.is_in_equilibrium(verbose=False) # In[104]: uc.plot_history(show_intervals=True, title_prefix=f"E0 = {E_init}") # In[105]: # Locate the intersection of the curves for [S] and [P]: uc.curve_intersect("S", "P", t_start=0, t_end=0.005) # #### The timescales of [S] and [P] continues to become faster with a higher initial [E] -- **AND REMAIN COMPARABLE TO THE TIMESCALES OF E AND ES** # #### At these high initial concentrations of [E], the time curves of [E] and [ES] no longer overlapping, and are now getting further apart # In[106]: uc.get_history(columns=['SYSTEM TIME', 'E', 'ES', 'P'], tail=1) # Last point in the simulation # In[107]: P_equil = uc.get_chem_conc("P") P_equil # #### P_equil continues to decrease with increasing initial [E] # In[108]: P_70_threshold = P_equil * 0.70 P_70_threshold # In[109]: uc.reach_threshold(chem="P", threshold=P_70_threshold) # In[ ]: # # CONCLUSION: # ### as the initial concentration of the Enzyme E increases from zero to lavish values much larger that the concentration of the Substrate S, the reaction keeps reaching an equilibrium faster and faster... # #### BUT the equilibrium concentration of the Product P steadily _reduces_ - and ES, far from being transient, actually builds up. # #### The **strange world of departing from the customary Michaelis-Menten assumptions** that `[E] << [S]` and that the rates satisfy `k1_reverse >> k2_forward` ! # In[110]: uc.get_history(head=1) # First point in the simulation # In[111]: uc.get_history(tail=1) # Last point in the simulation # ### When the initial [E] is quite high, relatively little of the reactant S goes into making the product P ; most of S binds with the abundant E, to produce a lasting ES # The following manual stoichiometry check illustrates it. # #### Manual overall STOICHIOMETRY CHECK: # In[112]: delta_S = 0.46627 - 20.0 delta_S # In[113]: delta_E = 82.779099 - 100.0 delta_E # In[114]: delta_E = 82.779099 - 100.0 delta_E # In[115]: delta_ES = 17.220901 - 0 delta_ES # In[116]: rxns.describe_reactions() # In[ ]: # 19.53373 units of `S` are consumed : a meager 2.312829 of that goes into the production of `P` (rxn 0) ; the remainder of Delta S is: # In[117]: 19.53373 - 2.312829 # That extra Delta S of 17.2209 combines with an equal amount of `E` to produce the same amount, 17.2209, of ES (rxn 1) # # `E`, now depleted by that amount of 17.2209, reaches the value: # In[118]: 100 - 17.2209 # In[119]: # Also review that the chemical equilibrium holds, with the final simulation values uc.is_in_equilibrium() # In[ ]: