Flux balance analysis#
Flux balance analysis (FBA) predicts steady-state fluxes in a metabolic network. For a stoichiometric matrix \(S\) and reaction-flux vector \(v\), FBA imposes
and optimizes an objective such as biomass production. These models are commonly used to predict growth, compare media, simulate knockouts, and identify metabolic-engineering strategies.
CORNETO brings FBA into the same optimization framework used for other network-inference problems. It interoperates with COBRApy models and supports standard FBA, while exposing the resulting optimization problem so that constraints, conditions, sparsity, and omics-derived evidence can be combined through a common interface.
COBRApy remains a natural tool for curating metabolic models and running established COBRA workflows. CORNETO is useful when the formulation itself must be extended or several network-inference problems must be expressed together.
From COBRApy to CORNETO#
COBRApy concept |
CORNETO concept |
|---|---|
|
metabolic hypergraph |
metabolite |
vertex |
reaction and stoichiometry |
directed hyperedge and its coefficients |
reaction bounds |
edge-flow bounds |
objective coefficients |
reaction objectives |
|
|
optimize a configured model |
build and extend an optimization problem |
The graph representation preserves reaction identifiers, stoichiometry, default bounds, and gene–protein–reaction annotations imported from the COBRApy model.
Solve the COBRApy model#
We start with the familiar E. coli core model distributed with COBRApy.
from contextlib import redirect_stdout
from io import StringIO
import numpy as np
import pandas as pd
from cobra.io import load_model
from corneto.io import cobra_model_to_graph
from corneto.methods import MultiSampleFBA
with redirect_stdout(StringIO()):
model = load_model("textbook")
biomass_id = "Biomass_Ecoli_core"
cobra_solution = model.optimize()
pd.Series(
{
"metabolites": len(model.metabolites),
"reactions": len(model.reactions),
"biomass flux": cobra_solution.fluxes[biomass_id],
},
name="COBRApy model",
)
metabolites 72.000000
reactions 95.000000
biomass flux 0.873922
Name: COBRApy model, dtype: float64
Convert the model and solve with CORNETO#
cobra_model_to_graph converts each reaction into a stoichiometric hyperedge. MultiSampleFBA.build then constructs the mass-balance constraints, applies the imported bounds, and adds the requested reaction objective.
MultiSampleFBA minimizes a weighted objective. Therefore, the coefficient -1 below maximizes biomass. CORNETO’s general optimization API also supports an explicit Direction.MAX when constructing a problem directly, but direction is not an argument of MultiSampleFBA.build.
G = cobra_model_to_graph(model)
reaction_ids = list(G.get_attr_from_edges("id"))
reaction_index = {reaction_id: i for i, reaction_id in enumerate(reaction_ids)}
biomass_idx = reaction_index[biomass_id]
problem = MultiSampleFBA().build(
G,
objectives={biomass_id: -1},
)
problem.solve(solver="scipy")
corneto_biomass = float(problem.expr.flow[biomass_idx].value)
comparison = pd.Series(
{
"COBRApy": cobra_solution.fluxes[biomass_id],
"CORNETO": corneto_biomass,
},
name="optimal biomass flux",
)
assert np.isclose(comparison["COBRApy"], comparison["CORNETO"], atol=1e-7)
comparison
COBRApy 0.873922
CORNETO 0.873922
Name: optimal biomass flux, dtype: float64
Fluxes follow the graph’s edge order. Mapping that order back to reaction identifiers gives a labeled result analogous to solution.fluxes.
fluxes = pd.Series(
np.asarray(problem.expr.flow.value),
index=reaction_ids,
name="flux",
)
fluxes.loc[[biomass_id, "EX_glc__D_e", "EX_o2_e", "ATPM"]]
Biomass_Ecoli_core 0.873922
EX_glc__D_e -10.000000
EX_o2_e -21.799493
ATPM 8.390000
Name: flux, dtype: float64
Extend the optimization problem#
A built CORNETO problem is still editable. For example, the following constraint limits oxygen uptake to 10 mmol gDW\(^{-1}\) h\(^{-1}\). In this model, exchange uptake is a negative flux, so a less-negative lower limit permits less uptake.
oxygen_idx = reaction_index["EX_o2_e"]
oxygen_limited = MultiSampleFBA().build(
G,
objectives={biomass_id: -1},
)
oxygen_limited += oxygen_limited.expr.flow[oxygen_idx] >= -10
oxygen_limited.solve(solver="scipy")
oxygen_limited_biomass = float(oxygen_limited.expr.flow[biomass_idx].value)
assert oxygen_limited_biomass < corneto_biomass
pd.Series(
{
"default medium": corneto_biomass,
"oxygen-limited": oxygen_limited_biomass,
},
name="optimal biomass flux",
)
default medium 0.873922
oxygen-limited 0.559051
Name: optimal biomass flux, dtype: float64
Solve multiple media conditions#
build_many creates one flux vector per named condition. Objectives and reaction bounds use an outer mapping with the same condition names. Conditions are solved in one problem, which also makes it possible for advanced formulations to couple them.
condition_objectives = {
"glucose_rich": {biomass_id: -1},
"glucose_limited": {biomass_id: -1},
}
condition_bounds = {
"glucose_rich": {
"EX_glc__D_e": (-10.0, 1000.0),
"EX_o2_e": (-20.0, 1000.0),
},
"glucose_limited": {
"EX_glc__D_e": (-2.0, 1000.0),
"EX_o2_e": (-20.0, 1000.0),
},
}
media_problem = MultiSampleFBA().build_many(
G,
objectives=condition_objectives,
reaction_bounds=condition_bounds,
)
media_problem.solve(solver="scipy")
condition_names = list(condition_objectives)
reported_reactions = [biomass_id, "EX_glc__D_e", "EX_o2_e"]
media_fluxes = pd.DataFrame(
{
condition: {
reaction_id: float(media_problem.expr.flow[reaction_index[reaction_id], column].value)
for reaction_id in reported_reactions
}
for column, condition in enumerate(condition_names)
}
)
assert media_problem.expr.flow.value.shape == (G.num_edges, len(condition_names))
assert media_fluxes.loc[biomass_id, "glucose_limited"] < media_fluxes.loc[biomass_id, "glucose_rich"]
media_fluxes
| glucose_rich | glucose_limited | |
|---|---|---|
| Biomass_Ecoli_core | 0.832614 | 0.140604 |
| EX_glc__D_e | -10.000000 | -2.000000 |
| EX_o2_e | -20.000000 | -5.853994 |
Sparse FBA#
CORNETO can associate each reaction with a binary indicator that records whether its flux is nonzero. With lambda_reg > 0, it penalizes the number of active reactions. Because of these indicators, sparse FBA is a mixed-integer linear program (MILP), rather than the linear program used by the standard mathematical FBA formulation.
Here we require at least 90% of the optimal biomass and optimize biomass together with a penalty on the number of active reactions. lambda_reg controls that trade-off; larger values give reaction count more influence. The biomass floor prevents sparsity from being achieved by suppressing growth.
biomass_floor = 0.90 * corneto_biomass
sparse_problem = MultiSampleFBA(lambda_reg=0.1).build(
G,
objectives={biomass_id: -1},
reaction_bounds={biomass_id: (biomass_floor, None)},
)
sparse_problem.solve(solver="highs")
sparse_biomass = float(sparse_problem.expr.flow[biomass_idx].value)
standard_active = int(np.count_nonzero(np.abs(problem.expr.flow.value) > 1e-6))
sparse_active = int(np.count_nonzero(np.abs(sparse_problem.expr.flow.value) > 1e-6))
assert sparse_biomass >= biomass_floor - 1e-7
assert sparse_active <= standard_active
pd.Series(
{
"biomass floor": biomass_floor,
"sparse biomass": sparse_biomass,
"active reactions in standard solution": standard_active,
"active reactions in sparse solution": sparse_active,
}
)
biomass floor 0.786529
sparse biomass 0.814298
active reactions in standard solution 48.000000
active reactions in sparse solution 45.000000
dtype: float64
This reaction-count criterion is not the same as parsimonious FBA (pFBA), which conventionally minimizes total flux after fixing the primary objective. Sparse FBA instead penalizes how many reactions are active.
For several conditions, CORNETO can apply structured sparsity to the union of active reactions. A reaction shared by several conditions is then counted once, encouraging compact shared metabolic programs while retaining a separate flux vector for every condition. Continue with Multi-condition FBA for the general formulation and a worked example, followed by gene expression integration for context-specific metabolic inference.
The examples above use the method-specific build and build_many interfaces. The general Data interface remains available for workflows that require custom feature metadata or already represent measurements as CORNETO data objects.