Building a model from scratch#
A hand-built model starts with metabolites, a reaction, a gene association and the exchanges that let material in and out, and ends with a model that carries flux. The same steps test an idea before an automated reconstruction is involved.
Functions#
MATLAB |
Python |
Purpose |
|---|---|---|
add metabolites |
||
add reactions from equation strings |
||
auto-created from the GPR cobrapy |
add genes |
|
add exchange reactions |
||
read the equations back |
In both toolboxes, write a reaction as an equation string and let the toolbox derive the stoichiometry, not a column of the stoichiometric matrix by hand.
Setup#
empty.xml is RAVEN's starter model: four metabolites (sucrose, glucose, fructose and water) and one reaction, invertase, which splits sucrose into glucose and fructose. Everything is in one compartment, e.
model = importModel('empty.xml');
fprintf('%d reactions, %d metabolites\n', numel(model.rxns), numel(model.mets));
eqn = constructEquations(model);
fprintf('%s\n', eqn{1});
The model contains 0 errors and 1 warnings.
[Warning: The following fields have prefixes removed from all entries. If this is undesired, run importModel with removePrefix as false. Example: importModel('filename.xml',[],false); model.rxns (R_ prefix) model.mets (M_ prefix)]
1 reactions, 4 metabolites
sucrose[e] + H2O[e] => glucose[e] + fructose[e]
from cobra.io import read_sbml_model
model = read_sbml_model("empty.xml")
print(len(model.reactions), "reactions,", len(model.metabolites), "metabolites")
for reaction in model.reactions:
print(f" {reaction.id}: {reaction.reaction}")
1 reactions, 4 metabolites
r1: m1 + m4 --> m2 + m3
Adding metabolites#
Hexokinase is added next, so the model needs ATP, ADP and glucose-6-phosphate first.
metsToAdd.mets = {'m5', 'm6', 'm7'};
metsToAdd.metNames = {'ATP', 'ADP', 'glucose-6-phosphate'};
metsToAdd.compartments = 'e';
metsToAdd.metFormulas = {'C10H12N5O13P3', 'C10H12N5O10P2', 'C6H11O9P'};
model = addMets(model, metsToAdd);
fprintf('%d metabolites\n', numel(model.mets));
7 metabolites
from cobra import Metabolite
model.add_metabolites([
Metabolite("m5", name="ATP", formula="C10H12N5O13P3", compartment="e"),
Metabolite("m6", name="ADP", formula="C10H12N5O10P2", compartment="e"),
Metabolite("m7", name="glucose-6-phosphate", formula="C6H11O9P", compartment="e"),
])
print(len(model.metabolites), "metabolites")
7 metabolites
Give every metabolite a formula and a compartment when it is added. Quality control checks against both, and adding them later means revisiting every reaction written in between.
Adding a reaction#
The equation string is the same in both toolboxes: <=> for a reversible reaction, => for an irreversible one.
rxnsToAdd.rxns = {'HEX1'};
rxnsToAdd.rxnNames = {'hexokinase'};
rxnsToAdd.equations = {'m2 + m5 => m7 + m6'};
rxnsToAdd.grRules = {'YFR053C'};
model = addRxns(model, rxnsToAdd, 'eqnType', 1, 'allowNewGenes', true);
idx = getIndexes(model, 'HEX1', 'rxns');
eqn = constructEquations(model, {'HEX1'});
fprintf('%s\n', eqn{1});
fprintf('bounds: [%g %g], genes: %s\n', model.lb(idx), model.ub(idx), ...
model.grRules{idx});
New genes added to the model:
YFR053C
glucose[e] + ATP[e] => ADP[e] + glucose-6-phosphate[e]
bounds: [0 Inf], genes: YFR053C
eqnType specifies how the equation is written: 1 or "id" matches metabolites against model.mets, 2 or "name" against model.metNames with compartment deciding where they go, and 3 or "name[comp]" reads the compartment from the token itself.
allowNewGenes is needed because YFR053C is not in the model. Both allowNewGenes and allowNewMets default to false, so addRxns refuses anything it does not recognise unless the option is set, and a mistyped identifier is an error, not a new entity. allowNewMets also accepts a string, used as the prefix for the ids it generates. Adding metabolites with addMets first carries more annotation than addRxns can infer.
from raven_toolbox.manipulation import add_reactions_from_equations
add_reactions_from_equations(model, [
{
"id": "HEX1",
"name": "hexokinase",
"equation": "m2 + m5 => m7 + m6",
"gene_reaction_rule": "YFR053C",
},
])
hex1 = model.reactions.get_by_id("HEX1")
print(hex1.reaction)
print("bounds:", hex1.bounds, "genes:", hex1.gene_reaction_rule)
m2 + m5 --> m6 + m7
bounds: (0.0, 1000.0) genes: YFR053C
The arrow sets the bounds, so an irreversible reaction needs no bounds key; passing bounds overrides it. <=> is reversible, and -->, -> and => are accepted for an irreversible reaction.
allow_new_genes and allow_new_mets both default to True here, the opposite of the MATLAB defaults, so a gene named in the rule is created without a separate step and without a message. That removes the addGenesRaven call MATLAB needs, and a typo becomes a new gene without an error. Set them to False once the model's metabolites and genes are all defined.
Adding exchanges#
Without boundary reactions nothing can enter or leave, so there is no net uptake, secretion or growth. Internal cycles can carry flux even without boundary reactions (see Flux variability and alternative optima), and under the steady-state assumption a metabolite that appears in only one reaction blocks that reaction. Exchange reactions are the boundary.
model = addExchangeRxns(model, 'both', 'mets', model.mets);
fprintf('%d reactions\n', numel(model.rxns));
NOTE: The exchange reactions are assigned to the first compartment
9 reactions
for metabolite in list(model.metabolites):
model.add_boundary(metabolite, type="exchange")
print(len(model.reactions), "reactions")
print(sorted(rxn.id for rxn in model.exchanges))
9 reactions
['EX_m1', 'EX_m2', 'EX_m3', 'EX_m4', 'EX_m5', 'EX_m6', 'EX_m7']
add_boundary works out that e is the external compartment. It refuses when nothing looks external, which is the usual reason it fails on a small hand-built model: give a compartment a recognisable name, or build the exchange as an ordinary reaction with a single metabolite. type="demand" and type="sink" add the other two kinds of boundary reaction, which cobrapy tracks in separate collections.
Flux check#
Solving the model shows whether the reactions assembled so far can carry flux at all.
model = setParam(model, 'obj', 'HEX1', 1);
sol = solveLP(model);
fprintf('HEX1 flux: %.2f\n', sol.x(getIndexes(model, 'HEX1', 'rxns')));
HEX1 flux: 1000.00
model.objective = "HEX1"
solution = model.optimize()
print(f"HEX1 flux: {solution.fluxes['HEX1']:.2f}")
HEX1 flux: 1000.00
Troubleshooting
A typo creates a metabolite. The defaults differ:
add_reactions_from_equationscreates unrecognised metabolites and genes, whileaddRxnsrefuses them unlessallowNewMetsandallowNewGenesare set. Som7andM7become two different things in Python and an error in MATLAB. Setallow_new_mets=Falseandallow_new_genes=Falseonce everything is defined.No exchange reactions. Nothing can be taken up or secreted, so growth is zero, with no error to explain why.
A model with no external compartment.
add_boundarycannot guess one, and RAVEN'saddExchangeRxnsadds exchanges for internal metabolites without warning, opening the model to uptake and secretion of intermediates.
See also#
Editing an existing model, changing the contents of a model.
Quality control, checking a newly built model.
Reading and writing models, saving it.