'''
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 __future__ import absolute_import, division, print_function
import os
import numpy as np
from vivarium.library.dict_utils import tuplify_port_dicts
[docs]def get_molecules(reactions):
'''
Get a list of all molecules used by reactions
Args:
reaction (dict): all reactions that will be used by transport
Returns:
self.molecule_ids (list): 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):
'''
Make the rate law configuration, which tells the parameters where to be placed.
Args:
reactions (dict): all reactions that will be made into rate laws, in the same format as all_reactions (above).
Returns:
rate_law_configuration (dict): includes partition and reaction_cofactor entries for each reaction
'''
rate_law_configuration = {}
# 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, enzyme, cofactors_sets, partition, parameters):
'''
Make a convenience kinetics rate law for one enzyme
Args:
stoichiometry (dict): the stoichiometry for the given reaction
enzyme (str): 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 (dict): all the parameters with {parameter_id: value}
Returns:
a kinetic rate law for the reaction, with arguments for concentrations and parameters,
and returns flux.
'''
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 mol in cofactors]
positive_coeff = [c > 0 for c in coeff]
if all(c == True for c in positive_coeff): # if all coeffs are positive
kcat = -kcat_r # use reverse rate
elif all(c == False 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, rate_law_configuration, kinetic_parameters):
'''
Make a rate law for each reaction
Args:
reactions (dict): in the same format as all_reactions, described above
rate_law_configuration (dict): with an embedded structure:
{enzyme_id: {
'reaction_cofactors': {
reaction_id: [cofactors list]
}
'partition': [partition list]
}
}
kinetic_parameters (dict): with an embedded structure:
{reaction_id: {
'enzyme_id': {
parameter_id: value
}
}
}
Returns:
rate_laws (dict): each reaction_id is a key and has sub-dictionary for each relevant enzyme,
with kinetic rate law functions as their values
'''
rate_laws = {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 (dict): all metabolic reactions, with:
{reaction_id: {
'catalyzed by': list,
'is reversible': bool,
'stoichiometry': dict,
}}
kinetic_parameters (dict): a dictionary of parameters a nested format:
{reaction_id: {
enzyme_id : {
param_id: param_value}}}
Attributes:
rate_laws: a dict, with a key for each reaction id, and then subdictionaries with each reaction's enzymes
and their rate law function. These rate laws are used directly from within this dictionary
'''
def __init__(self, all_reactions, kinetic_parameters):
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):
'''
Use rate law functions to calculate flux
Args:
concentrations_dict (dict): all relevant molecules and their concentrations, in mmol/L.
{molecule_id: concentration}
Returns:
reaction_fluxes (dict) - with 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}
}
[docs]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()