"""
====================
Convenience Kinetics
====================
"""
import numpy as np
from vivarium.core.process import Process
from vivarium.core.composition import simulate_process
from ecoli.library.kinetic_rate_laws import KineticFluxModel
from ecoli.library.schema import numpy_schema, bulk_name_to_idx, counts
NAME = "enzyme_kinetics"
[docs]
class EnzymeKinetics(Process):
"""Michaelis-Menten-style enzyme kinetics model
Arguments:
initial_parameters: Configures the :term:`process` with the
following configuration options:
* **reactions** (:py:class:`dict`): Specifies the
stoichiometry, reversibility, and catalysts of each
reaction to model. For a non-reversible reaction
:math:`A + B \\rightleftarrows 2C` catalized by an
enzyme :math:`E`, we have the following reaction
specification:
.. code-block:: python
{
# reaction1 is a reaction ID
'reaction1': {
'stoichiometry': {
# 1 mol A is consumd per mol reaction
('internal', 'A'): -1,
('internal', 'B'): -1,
# 2 mol C are produced per mol reaction
('internal', 'C'): 2,
},
'is reversible': False,
'catalyzed by': [
('internal', 'E'),
],
}
}
Note that for simplicity, we assumed all the molecules
and enzymes were in the ``internal`` port, but this is
not necessary.
* **kinetic_parameters** (:py:class:`dict`): Specifies
the kinetics of the reaction by providing
:math:`k_{cat}` and :math:`K_M` parameters for each
enzyme. For example, let's say that for the reaction
described above, :math:`k{cat} = 1`, :math:`K_A = 2`,
and :math:`K_B = 3`. Then the reaction kinetics would
be specified by:
.. code-block:: python
{
'reaction1': {
('internal', 'E'): {
'kcat_f': 1, # kcat for forward reaction
('internal', 'A'): 2,
('internal', 'B'): 3,
},
},
}
If the reaction were reversible, we could have
specified ``kcat_r`` as the :math:`k_{cat}` of the
reverse reaction.
"""
name = NAME
defaults: dict[str, dict] = {
"reactions": {},
"kinetic_parameters": {},
}
def __init__(self, parameters=None):
super().__init__(parameters)
self.reactions = self.parameters["reactions"]
kinetic_parameters = self.parameters["kinetic_parameters"]
# make the kinetic model
self.kinetic_rate_laws = KineticFluxModel(self.reactions, kinetic_parameters)
# remove "bulk" from the name
self.molecules_ids = [
mol_id[1] for mol_id in self.kinetic_rate_laws.molecule_ids
]
self.molecules_idx = None
# def initial_state(self, config):
# # TODO (Cyrus) - test if this works
# initial_conc = config['initial_concentrations']
# initial_fluxes = self.next_update(
# initial_conc, self.parameters['time_step'])
# return initial_fluxes
[docs]
def ports_schema(self):
schema = {
"bulk": numpy_schema("bulk"),
"fluxes": {
str(rxn_id): {
"_default": 0.0,
"_updater": "set",
}
for rxn_id in self.kinetic_rate_laws.reaction_ids
},
}
return schema
[docs]
def next_update(self, timestep, states):
if self.molecules_idx is None:
bulk_ids = states["bulk"]["id"]
self.molecules_idx = bulk_name_to_idx(self.molecules_ids, bulk_ids)
# TODO (Cyrus) -- convert molecules to concentrations
molecule_counts = counts(states["bulk"], self.molecules_idx)
tuplified_states = {
("bulk", mol): molecule_counts[i]
for i, mol in enumerate(self.molecules_ids)
}
# get flux, which is in units of mmol / L
fluxes = self.kinetic_rate_laws.get_fluxes(tuplified_states)
return {"fluxes": fluxes}
def test_enzyme_kinetics(end_time=100):
toy_reactions = {
"reaction1": {
"stoichiometry": {("bulk", "A"): 1, ("bulk", "B"): -1},
"is reversible": False,
"catalyzed by": [("bulk", "enzyme1")],
}
}
toy_kinetics = {
"reaction1": {
("bulk", "enzyme1"): {
("bulk", "B"): 0.2,
"kcat_f": 5e1,
}
}
}
config = {
"reactions": toy_reactions,
"kinetic_parameters": toy_kinetics,
}
kinetic_process = EnzymeKinetics(config)
initial_state = {
"bulk": np.array(
[("A", 1.0), ("B", 1.0), ("enzyme1", 1.0)],
dtype=[("id", "U7"), ("count", "f")],
)
}
settings = {"total_time": end_time, "initial_state": initial_state}
data = simulate_process(kinetic_process, settings)
return data is not None
# run module with python ecoli/processes/enzyme_kinetics.py
if __name__ == "__main__":
test_enzyme_kinetics()