15. Metabolic tasks#
A metabolic task is a statement of something the model must be able to do: "given glucose and oxygen, produce biomass", "make ATP", "do not produce something from nothing". Tasks are how a reconstruction is tested against biology rather than against itself, and they are what ftINIT repairs against after cutting a model down.
Functions on this page#
MATLAB |
Python |
|
|---|---|---|
read a task-list file |
||
run the tasks against a model |
||
|
reactions a task requires |
|
add reactions until the tasks pass |
The file format#
A task list is a tab-separated table, one row per task, with columns for the
inputs, the outputs, any equations that must carry flux, and the bounds on each.
tasks.txt holds two tasks for smallYeast.yml:
ID DESCRIPTION SHOULD FAIL IN IN LB IN UB OUT OUT LB OUT UB …
GROWTH Growth on glucose alpha-D-glucose[c];O2[c] 0 1000 biomass[c] 0.01 1000 …
CO2[c] 0 1000 …
acetate[c] 0 1000 …
LEAK Biomass from nothing 1 biomass[c] 0.01 1000 …
Two things about the format have to be right the first time, because both
fail in MATLAB with the same unhelpful Index in position 2 is invalid:
Every row starts with an empty cell.
parseTaskListdiscards any row whose first cell is non-empty (that is how it skips comments and section headers), so a file without the leading tab loses every row.Keep the full column set, even where the columns are empty.
parseTaskListmatches all seventeen known headers and then indexes the numeric ones positionally, so omittingEQU LBorCHANGED LBleaves an index of zero.
raven-toolbox's parse_task_list accepts the file with or without either, which
makes both easy to miss until the MATLAB tab runs. The file above has the full
set; the columns after OUT UB are blank.
Metabolites are named name[compartment], matching the metabolite names, not
the identifiers. Several go in one cell, separated by ;, and the bounds columns
apply to all of them. To give a metabolite bounds of its own, put it on a
continuation row: a row with no ID adds its inputs and outputs to the task
above it. GROWTH uses that to ask for at least 0.01 units of biomass while
leaving CO₂ and acetate free; with a lower bound of 0 on biomass, the all-zero
flux would pass the task. The 0.01 is illustrative; any positive floor makes
the point.
SHOULD FAIL marks a task the model must fail: the second asks for
biomass with nothing supplied, which a correct model cannot do, so failing it is
a pass.
Note what GROWTH has to list once the model's own exchanges are closed (15.2):
glucose and oxygen in, biomass out, and CO₂ and acetate, because growth
has to put its carbon somewhere: CO₂ and, in this small model, some acetate.
Leave CO₂ out and the task is infeasible, which looks like a broken model and is
really a broken task.
15.1 Read and run the tasks#
model = readYAMLmodel('smallYeast.yml');
tasks = parseTaskList('tasks.txt');
fprintf('%d tasks\n', numel(tasks));
closed = closeModel(model); % the task is the whole boundary, see 15.2
report = checkTasks(closed, [], 'printOutput', false, 'taskStructure', tasks);
for i = 1:numel(report.id)
verdicts = {'FAIL', 'pass'};
fprintf(' %-8s %s\n', report.id{i}, verdicts{report.ok(i) + 1});
end
2 tasks
GROWTH pass
LEAK pass
report.ok is true for both, and for opposite reasons: GROWTH was
required to pass and did, LEAK was required to fail and did.
checkTasks prints its own verdicts unless printOutput is off, as here,
and printOnlyFailed narrows that to the failures. Reading the verdicts
off report gives a structure to test against rather than text to read.
Passing the parsed tasks as taskStructure makes the second argument,
the task file, redundant. runParallel evaluates the tasks in parallel
workers, and defaults to false, because starting a pool takes longer than
the time it saves on a short task list; on a genome-scale list it is the
setting that matters.
from raven_toolbox.io import read_yaml_model
from raven_toolbox.tasks import check_tasks, parse_task_list
model = read_yaml_model("smallYeast.yml")
tasks = parse_task_list("tasks.txt")
print(len(tasks), "tasks")
for result in check_tasks(model, tasks, close_boundaries=True): # see 15.2
verdict = "pass" if result.passed else "FAIL"
print(f" {result.id:<8} {verdict} (feasible: {result.feasible})")
2 tasks
GROWTH pass (feasible: True)
LEAK pass (feasible: False)
close_boundaries=True closes the model's own exchange, sink and demand
reactions first, so the inputs and outputs are exactly what the task says they
are. That is why a task list is portable between models in a way a script full
of setParam calls is not.
15.2 A task defines its own medium#
By default neither checkTasks nor check_tasks closes the model's own
exchange reactions: a task's inputs and outputs are added to whatever the model
already allows. To make the task the whole boundary, close the model first:
closeModel in RAVEN (what ftINIT does before collecting task-essential
reactions), close_boundaries=True in raven-toolbox. The task's inputs and
outputs are then the only way anything enters or leaves, which is what makes
a task list portable between models, and what makes an incomplete task look like
a broken model.
noCO2 = tasks(1);
noCO2.id = 'GROWTH-NO-CO2';
keep = ~strcmp(noCO2.outputs, 'CO2[c]');
noCO2.outputs = noCO2.outputs(keep);
noCO2.LBout = noCO2.LBout(keep);
noCO2.UBout = noCO2.UBout(keep);
pair = [tasks(1); noCO2];
boundaries = {'open', 'closed'};
models = {model, closed};
for k = 1:2
report = checkTasks(models{k}, [], 'printOutput', false, 'taskStructure', pair);
for i = 1:numel(report.id)
fprintf(' %-6s %-14s feasible=%d\n', boundaries{k}, report.id{i}, report.ok(i));
end
end
[Warning: Exchange metabolites should normally not be removed from the model when using checkTasks. Inputs and outputs are defined in the task file instead. Use importModel(file,false) to import a model with exchange metabolites remaining]
open GROWTH feasible=1
open GROWTH-NO-CO2 feasible=1
closed GROWTH feasible=1
closed GROWTH-NO-CO2 feasible=0
from raven_toolbox.tasks import Task
growth = tasks[0]
no_co2 = Task(
id="GROWTH-NO-CO2",
description="the same task, without somewhere to put the CO2",
inputs=growth.inputs,
outputs=[o for o in growth.outputs if o[0] != "CO2[c]"],
)
for close in (False, True):
for result in check_tasks(model, [growth, no_co2], close_boundaries=close):
boundary = "closed" if close else "open"
print(f" {boundary:<6} {result.id:<14} feasible={result.feasible}")
open GROWTH feasible=True
open GROWTH-NO-CO2 feasible=True
closed GROWTH feasible=True
closed GROWTH-NO-CO2 feasible=False
The only difference between the two tasks is the CO₂ output. With the model's
own exchanges open, both are feasible: the carbon leaves through the model's
co2OUT, whatever the task says. With them closed, the task without CO₂ is
infeasible. A task list written for one reading gives different answers under
the other, so check which one it was curated against.
15.3 Which reactions does a task depend on?#
Essentiality with respect to a task, rather than to the objective: remove each candidate reaction and see whether the task still passes. This is what ftINIT uses to decide which reactions must survive an extraction regardless of their expression score.
[~, essentialRxns] = checkTasks(closed, [], 'printOutput', false, ...
'getEssential', true, 'taskStructure', tasks(1));
fprintf('%d reactions essential for the task\n', sum(essentialRxns));
28 reactions essential for the task
Task-essential reactions come from checkTasks with its getEssential
flag, returned as a reactions-by-tasks logical matrix rather than a list.
Failed tasks and SHOULD FAIL tasks are left out of it, since a task that
does not pass has no reactions it depends on. getEssentialRxns answers
for the model's own objective and takes no task at all.
from raven_toolbox.tasks import find_task_essential_reactions
result = find_task_essential_reactions(model, tasks[:1], close_boundaries=True)
print(len(result.reactions), "reactions essential for the task")
print(sorted(result.reactions)[:5])
28 reactions essential for the task
['ACO', 'ACS', 'ALD6', 'CIT', 'CO2TRANS']
15.4 When a task fails#
A failing task is a gap: something the model should be able to do and cannot. Both toolboxes can add reactions from a template until the task passes, which is 17. Gap-filling, approached from the task side.
template = readYAMLmodel('smallYeast.yml'); % where the reactions come from
[outModel, addedRxns] = fitTasks(model, template, [], 'taskStructure', tasks);
from raven_toolbox.init import fill_tasks
fill_tasks is the same operation inside the ftINIT pipeline: given the
tasks and a source of reactions, it adds back the minimum needed to make
every task feasible again.
Warning
What can go wrong
Names, not identifiers. Task files address metabolites as
name[compartment]. A task that silently matches nothing is a task that passes for the wrong reason.Forgetting
SHOULD FAIL. The tasks that matter most are often the ones the model must not satisfy; without them, a leaking model passes everything.Not knowing which boundary the task ran against. With the model closed, a task defines its own inputs and does not inherit the model's medium, so a task passing says nothing about whether the model grows on the medium it carries. Left open (the default in both toolboxes), the model's own exchanges take part, and a task can pass because of an uptake or secretion it never lists.
See also#
17. Gap-filling, making a failing task pass.
14. Context-specific models, tasks as the thing an extraction must preserve.
9. Quality control, the checks that do not need a task list.