Network Analysis

StrainDesign provides canonical functions for maximizing and minimizing metabolic fluxes in network. The output format is identical to the ones of COBRApy’s functions.

Flux optimization (FBA/pFBA)

Flux Balance Analysis (FBA) is a linear program that optimizes a flux rate under the given steady-state network constraints. For the case of a growth-rate maximization, the problem is written as:

\[\begin{split}\text{maximize}\:\:\: v_{growth} \\ \text{ subject to } \\ \mathbf{S\, v = 0 } \\ \mathbf{ lb \le v \le ub}\end{split}\]

Where \(\mathbf{S}\) is the stoichiometric matrix of the metabolic model, \(\mathbf{v}\) is the vector of metabolic flux rates and \(\mathbf{lb}\) and \(\mathbf{ub}\) are the physiological lower and upper bounds of each flux rate which also define whether a reaction can run in the reverse direction (\(\mathbf{lb} < 0\)) or not (\(\mathbf{lb} \ge 0\)). \(\mathbf{S\, v = 0}\) represents all steady-state constraints, and \(\mathbf{lb \le v \le ub}\) the allowed flux ranges.

Note:

All of the following computation examples will require the COBRApy and the StrainDesign package.

Here, we load both packages and the e_coli_core model from BiGG:

[1]:
import cobra
import straindesign as sd
model = cobra.io.load_model('e_coli_core')
Set parameter Username
Academic license - for non-commercial use only - expires 2023-07-20

An FBA is launched by a single function call. By default the model’s objective function is optimized. The function returns a solution object, in which the objective value and the fluxes are stored in solution.objective_value and solution.fluxes.

[2]:
solution = sd.fba(model)

print(f"Maximum growth: {solution.objective_value}.")
Maximum growth: 0.8739215069684305.

You may also use a custom objective (in form of a linear expression) and change the optimization sense. Here we minimize the Glucose uptake rate through the PTS:

[3]:
solution = sd.fba(model,obj='GLCpts',obj_sense='minimize')

print(f"Minimum flux through GLCpts: {solution.objective_value}.")
Minimum flux through GLCpts: 0.4794285714285715.

We can also consider custom constraints (in this case limited oxygen uptake and an increased fixed ATP maintenance demand):

[4]:
solution = sd.fba(model,constraints=['-EX_o2_e <= 5', 'ATPM = 20'])

print(f"Maximum growth at limited oxygen uptake and high ATP maintenance: {solution.objective_value}.")
Maximum growth at limited oxygen uptake and high ATP maintenance: 0.26305573292588313.

Parsimonious FBA (pFBA)

Parsimonious flux balance analysis optimizes a flux rate under the given steady-state network constraints, but also minimizes the sum of absolute fluxes to achieve this optimum. One can write:

\[\begin{split}\text{minimize}\:\:\:\Sigma | v_i | \\ \text{subject to} \\\end{split}\]
\[\begin{split}\text{maximize}\:\:\: v_{growth} \\ \text{subject to} \\ \mathbf{S\, v = 0} \\ \mathbf{lb \le v \le ub}\end{split}\]

pFBA is the simpleset (although very rough) approach of emulating a cell’s enzyme cost minimization (after an assumed growth maximization).

Note:

pFBA solutions are more often “unique” than pure FBA solutions, since the outer minimization leaves fewer degrees of freedom in the solution space.

StrainDesign computes pFBA solutions when you pass the ‘pFBA’-argument with a value of 1.

[5]:
fba_sol = sd.fba(model)
pfba1_sol = sd.fba(model,pfba=1)
print(f"The sum of fluxes of the regular FBA: {sum([abs(v) for v in fba_sol.fluxes.values()])} "+\
      f"is usually higher than of the parsimoniuos FBA: {sum([abs(v) for v in pfba1_sol.fluxes.values()])}")
The sum of fluxes of the regular FBA: 2508.293334194643 is usually higher than of the parsimoniuos FBA: 518.4220855176071

Likewise, it is possible to minimize the number of active reactions to attain an optimal flux distribution. We therefore use pFBA mode 2. Most of the times modes 1 and 2 yield the same results.

[6]:
pfba1_sol = sd.fba(model,pfba=1)
pfba2_sol = sd.fba(model,pfba=2)
print(f"The number of active reactions in pFBA1: {sum([v!=0 for v in pfba1_sol.fluxes.values()])}, "+\
      f"and pFBA2: {sum([v!=0 for v in pfba2_sol.fluxes.values()])}, is often identical.")
The number of active reactions in pFBA1: 48, and pFBA2: 48, is often identical.

Flux variability analysis (FVA)

The fva function determines the possible maximal and minimal flux ranges under the given model constraints:

[7]:
solution = sd.fva(model)
print(solution)
         minimum  maximum
PFK          0.0   176.61
PFL          0.0    40.00
PGI        -50.0    10.00
PGK        -20.0    -0.00
PGL          0.0    60.00
...          ...      ...
NADH16       0.0   120.00
NADTRHD      0.0   378.22
NH4t         0.0    10.00
O2t          0.0    60.00
PDH          0.0    40.00

[95 rows x 2 columns]

The parameters ‘solver’ and ‘constraints’ can also be used in the FVA function call. As an example, we determine the flux ranges for the case that the flux sum of PDH and PFL is smaller than 8.

[8]:
solution = sd.fva(model, constraints='PDH + PFL <= 8')
print(solution)
         minimum     maximum
PFK          0.0  147.610000
PFL          0.0    8.000000
PGI        -50.0   10.000000
PGK        -20.0   -0.000000
PGL          0.0   60.000000
...          ...         ...
NADH16       0.0  120.000000
NADTRHD      0.0  375.220000
NH4t         0.0    8.300263
O2t          0.0   60.000000
PDH          0.0    8.000000

[95 rows x 2 columns]

Yield optimization

Yield optmization aims to maximize a given flux expression (e.g., a product’s synthesis rate) divided by another such expression (e.g., a substrate’s uptake rate).

Warning:

Yield optimization and Flux Balance Analysis are two different methods that produce distinct optimal values and also distinct optimal flux vectors.

Consider the following example of maximizing growth and biomass yield under limited oxygen uptake.

[9]:
numerator = 'BIOMASS_Ecoli_core_w_GAM'
denominator = '-EX_glc__D_e'
constraint = '-EX_o2_e <= 3'

sol_fba = sd.fba(model,obj='BIOMASS_Ecoli_core_w_GAM',obj_sense='maximize',constraints=constraint)
fba_yield = sol_fba.fluxes[numerator] / -sol_fba.fluxes['EX_glc__D_e']

sol_yopt = sd.yopt(model,obj_num=numerator,obj_den=denominator,obj_sense='maximize',constraints=constraint)
yopt_yield = sol_yopt.objective_value

print(f"Maximum yield (FBA): {fba_yield}.")
print(f"Maximum yield (yOpt): {yopt_yield}.")
Maximum yield (FBA): 0.031965425062067315.
Maximum yield (yOpt): 0.03629426243040193.

The best biomass yield is achieved when only respiration is used. The best growth rates use respiration and additionally overflow metabolism with poorer biomass yield.

The plotting chapter shows how relationships between yields and rates can be visualized.

Mathematical background

Constraint-based models (\(\mathbf{S\, v = 0}\), \(\mathbf{lb \le v \le ub}\)) can be rewritten in a single matrix-inequality term (\(\mathbf{A \, x \le b}\)).

With this notation, the yield optimization is a linear fractional program (LFP):

\[\begin{split}\begin{array}{l c} \text{maximize} \; &\mathbf{\tfrac{c^\intercal x}{d^\intercal x}} \\ \text{subject to} &\mathbf{A \, x \le b}. \end{array}\end{split}\]

Under the condition that the denominator term is strictly positive (\(\mathbf{A\,x\le b}\, \Rightarrow\, \mathbf{d}^\intercal \mathbf{x}>\,0\)), the LFP may be rewritten as an LP problem, using the Charnes-Cooper transformation. The formerly fixed boundaries \(\mathbf{b}\) of the problem are then scaled by the auxiliary variable \(e\) while the variable \(y = \mathbf{\tfrac{c^\intercal x}{d^\intercal x}}\) expresses the original objective function:

\[\begin{split}\begin{array}{l c} \text{maximize} \; & y \\\\ \text{subject to} & \begin{bmatrix} \mathbf{A~} & \mathbf{-b} & \mathbf{0} \\ \mathbf{d}^\intercal & 0 & 0 \\ \mathbf{c}^\intercal & 0 & -1 \\ \end{bmatrix} \begin{bmatrix}\mathbf{\tilde{x}} \\ e \\ y \end{bmatrix} \begin{matrix} \le \\ = \\ = \end{matrix} \begin{bmatrix}\mathbf{0} \\ 1 \\ 0 \end{bmatrix}\\\\ &e \ge 0. \end{array}\end{split}\]

Solutions of \(\mathbf{x}\) to the LFP (first problem) can be retrieved from a solution of the LP through \(\mathbf{x}=\frac{\mathbf{\tilde{x}}}{e}\).