Source code for ecoli.library.kinetic_rate_laws

"""
Kinetic rate law generation using the Convenience Kinetics formulation of Michaelis-Menten kinetics

Formulation provided in:
    Liebermeister, Wolfram, and Edda Klipp. "Bringing metabolic networks to life:
    convenience rate law and thermodynamic constraints."
    Theoretical Biology and Medical Modelling 3.1 (2006): 41.

# TODO -- make a vmax options if enzyme kcats not available
"""

from typing import Any, Callable

import numpy as np

from vivarium.library.dict_utils import tuplify_port_dicts


[docs] def get_molecules(reactions: dict[str, dict]) -> list[str]: """ Get a list of all molecules used by reactions Args: reaction: all reactions that will be used by transport Returns: all molecules used by these reactions """ molecule_ids = [] for reaction_id, specs in reactions.items(): stoichiometry = specs["stoichiometry"] substrates = stoichiometry.keys() enzymes = specs["catalyzed by"] # Add all relevant molecules_ids molecule_ids.extend(substrates) molecule_ids.extend(enzymes) return list(set(molecule_ids))
# Helper functions
[docs] def make_configuration(reactions: dict) -> dict: """ Make the rate law configuration, which tells the parameters where to be placed. Args: reactions: all reactions that will be made into rate laws, in the same format as all_reactions (above). Returns: Dictionary of partition and reaction_cofactor entries for each reaction """ rate_law_configuration: dict[str, dict[str, Any]] = {} # gets all potential interactions between the reactions for reaction_id, specs in reactions.items(): enzymes = specs["catalyzed by"] # initialize all enzymes for enzyme in enzymes: if enzyme not in rate_law_configuration: rate_law_configuration[enzyme] = { "partition": [], "reaction_cofactors": {}, } # identify parameters for reactions for reaction_id, specs in reactions.items(): stoich = specs.get("stoichiometry") enzymes = specs.get("catalyzed by", None) reversibility = specs.get("is reversible", False) # get sets of cofactors driving this reaction forward_cofactors = [mol for mol, coeff in stoich.items() if coeff < 0] cofactors = [forward_cofactors] if reversibility: reverse_cofactors = [mol for mol, coeff in stoich.items() if coeff > 0] cofactors.append(reverse_cofactors) # get partition, reactions, and parameter indices for each enzyme, and save to rate_law_configuration dictionary for enzyme in enzymes: # get competition for this enzyme from all other reactions competing_reactions = [ rxn for rxn, specs2 in reactions.items() if (rxn is not reaction_id) and (enzyme in specs2["catalyzed by"]) ] competitors = [] for reaction2 in competing_reactions: stoich2 = reactions[reaction2]["stoichiometry"] reactants2 = [mol for mol, coeff in stoich2.items() if coeff < 0] competitors.append(reactants2) # partition includes both competitors and cofactors. partition = competitors + cofactors rate_law_configuration[enzyme]["partition"] = partition rate_law_configuration[enzyme]["reaction_cofactors"][reaction_id] = ( cofactors ) return rate_law_configuration
[docs] def cofactor_numerator(concentration, km): return concentration / km if km else 0
[docs] def cofactor_denominator(concentration, km): return 1 + concentration / km if km else 1
[docs] def construct_convenience_rate_law( stoichiometry: dict[str, int], enzyme: str, cofactors_sets: list[list[str]], partition: list[list[str]], parameters: dict[str, Any], ) -> Callable: """ Make a convenience kinetics rate law for one enzyme Args: stoichiometry: the stoichiometry for the given reaction enzyme: the current enzyme cofactors_sets: a list of lists with the required cofactors, grouped by [[cofactor set 1], [cofactor set 2]], each pair needs a kcat. partition: a list of lists. each sublist is the set of cofactors for a given partition. [[C1, C2],[C3, C4], [C5]] parameters: all the parameters with ``{parameter_id: value}`` Returns: a kinetic rate law function that returns flux through the given reaction with a dictionary of molecule concentrations as input """ kcat_f = parameters.get("kcat_f") kcat_r = parameters.get("kcat_r") # remove km parameters with None as their value for parameter, value in parameters.items(): if "kcat" not in parameter: if value is None: for part in partition: if parameter in part: part.remove(parameter) for cofactors_set in cofactors_sets: if parameter in cofactors_set: cofactors_set.remove(parameter) # print('removing parameter: {}'.format(parameter)) # if reversible, determine direction by looking at stoichiometry if kcat_r: coeff = [ stoichiometry[mol] for cofactors in cofactors_sets for mol in cofactors ] positive_coeff = [c > 0 for c in coeff] if all(positive_coeff): # if all coeffs are positive kcat = -kcat_r # use reverse rate elif all(not c for c in positive_coeff): # if all coeffs are negative kcat = kcat_f else: kcat = kcat_f def rate_law(concentrations): # construct numerator enzyme_concentration = concentrations[enzyme] numerator = 0 for cofactors in cofactors_sets: # multiply the affinities of all cofactors term = np.prod( [ cofactor_numerator( concentrations[molecule], parameters[molecule] ) # km of molecule for molecule in cofactors ] ) numerator += kcat * term # TODO (if there is no kcat, need an exception) numerator *= enzyme_concentration # construct denominator, with all competing terms in the partition # denominator starts at +1 for the unbound state denominator = 1 for cofactors_set in partition: # multiply the affinities of all cofactors in this partition term = np.prod( [ cofactor_denominator(concentrations[molecule], parameters[molecule]) for molecule in cofactors_set ] ) denominator += term - 1 flux = numerator / denominator return flux return rate_law
# Make rate laws
[docs] def make_rate_laws( reactions: dict, rate_law_configuration: dict, kinetic_parameters: dict ) -> dict[str, dict[str, Callable]]: """ Make a rate law for each reaction Args: reactions: in the same format as all_reactions, described above rate_law_configuration: with an embedded structure:: {enzyme_id: { 'reaction_cofactors': { reaction_id: [cofactors list] } 'partition': [partition list] } } kinetic_parameters: with an embedded structure:: {reaction_id: { 'enzyme_id': { parameter_id: value } } } Returns: Dictionary where each reaction_id is a key and each value is a sub-dictionary with kinetic rate law functions for each enzyme """ rate_laws: dict[str, dict] = { reaction_id: {} for reaction_id in list(reactions.keys()) } for reaction_id, specs in reactions.items(): stoichiometry = specs.get("stoichiometry") # reversible = specs.get('is reversible') # TODO (eran) -- add reversibility based on specs enzymes = specs.get("catalyzed by") # rate law for each enzyme for enzyme in enzymes: if enzyme not in kinetic_parameters[reaction_id]: print("{} not in reaction {}".format(enzyme, reaction_id)) continue cofactors_sets = rate_law_configuration[enzyme]["reaction_cofactors"][ reaction_id ] partition = rate_law_configuration[enzyme]["partition"] rate_law = construct_convenience_rate_law( stoichiometry, enzyme, cofactors_sets, partition, kinetic_parameters[reaction_id][enzyme], ) # save the rate law for each enzyme in this reaction rate_laws[reaction_id][enzyme] = rate_law return rate_laws
[docs] class KineticFluxModel(object): """ A kinetic rate law class Args: all_reactions: all metabolic reactions, with:: {reaction_id: { 'catalyzed by': list, 'is reversible': bool, 'stoichiometry': dict, }} kinetic_parameters: a dictionary of parameters a nested format:: {reaction_id: { enzyme_id : { param_id: param_value}}} Attributes: rate_laws: Dictionary where each reaction_id is a key and each value is a sub-dictionary with kinetic rate law functions for each enzyme """ def __init__(self, all_reactions: dict, kinetic_parameters: dict): self.kinetic_parameters = kinetic_parameters self.reaction_ids = list(self.kinetic_parameters.keys()) self.reactions = { reaction_id: all_reactions[reaction_id] for reaction_id in all_reactions } self.molecule_ids = get_molecules(self.reactions) # make the rate laws self.rate_law_configuration = make_configuration(self.reactions) self.rate_laws = make_rate_laws( self.reactions, self.rate_law_configuration, self.kinetic_parameters )
[docs] def get_fluxes(self, concentrations_dict: dict[str, float]) -> dict[str, float]: """ Use rate law functions to calculate flux Args: concentrations_dict: all relevant molecules and their concentrations, in mmol/L. ``{molecule_id: concentration}`` Returns: Dictionary of fluxes for all reactions """ # Initialize reaction_fluxes and exchange_fluxes dictionaries reaction_fluxes = {reaction_id: 0.0 for reaction_id in self.reaction_ids} for reaction_id, enzymes in self.rate_laws.items(): for enzyme, rate_law in enzymes.items(): flux = rate_law(concentrations_dict) reaction_fluxes[reaction_id] += flux return reaction_fluxes
toy_reactions = { "ABC-13-RXN": { "stoichiometry": { ("cytoplasm", "PI"): 1, ("cytoplasm", "ADP"): 1, ("cytoplasm", "GLT"): 1, ("cytoplasm", "PROTON"): 1, ("cytoplasm", "ATP"): -1, ("cytoplasm", "WATER"): -1, ("periplasm", "GLT"): -1, }, "is reversible": False, "catalyzed by": [("membrane", "ABC-13-CPLX")], }, "TRANS-RXN-122": { "stoichiometry": { ("cytoplasm", "GLT"): 1, ("periplasm", "GLT"): -1, ("periplasm", "NA+"): -2, ("cytoplasm", "NA+"): 2, }, "is reversible": False, "catalyzed by": [("membrane", "GLTP-MONOMER"), ("membrane", "DCTA-MONOMER")], }, } toy_kinetics = { "ABC-13-RXN": { ("membrane", "ABC-13-CPLX"): { ("cytoplasm", "ATP"): None, ("periplasm", "GLT"): 1e-3, ("cytoplasm", "WATER"): None, "kcat_f": 1.0, } }, "TRANS-RXN-122": { ("membrane", "DCTA-MONOMER"): { ("periplasm", "GLT"): 1e-3, ("periplasm", "NA+"): 1e-5, "kcat_f": 1.0, }, ("membrane", "GLTP-MONOMER"): { ("periplasm", "GLT"): 1e-3, ("periplasm", "NA+"): 1e-5, ("periplasm", "PROTON"): None, "kcat_f": 1.0, }, }, } toy_initial_state = { "cytoplasm": { "PI": 1.0, "ADP": 1.0, "GLT": 1.0, "PROTON": 1.0, "ATP": 1.0, "WATER": 1.0, "NA+": 1.0, }, "periplasm": {"GLT": 1.0, "NA+": 1.0}, "membrane": {"ABC-13-CPLX": 1.0, "GLTP-MONOMER": 1.0, "DCTA-MONOMER": 1.0}, } def test_kinetics(): kinetic_rate_laws = KineticFluxModel(toy_reactions, toy_kinetics) flattened_toy_states = tuplify_port_dicts(toy_initial_state) flux = kinetic_rate_laws.get_fluxes(flattened_toy_states) print(flux) if __name__ == "__main__": test_kinetics()