Skip to content

Commit 48cbc6e

Browse files
committed
split pFBA into CT and easy-modeling part
1 parent 3d821fe commit 48cbc6e

File tree

5 files changed

+133
-135
lines changed

5 files changed

+133
-135
lines changed

docs/src/examples/03-qp-problems.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ model = A.load(J.JSONFBCModel, "e_coli_core.json") # load the model
2222

2323
# Use the convenience function to run standard pFBA
2424

25-
vt = X.parsimonious_flux_balance(model, Clarabel.Optimizer)
25+
vt = X.parsimonious_flux_balance(model, Clarabel.Optimizer; modifications = [X.silence])
2626

2727
# Or use the piping functionality
2828

src/analysis/flux_balance.jl

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,40 @@
11
"""
22
$(TYPEDSIGNATURES)
33
4-
Run flux balance analysis (FBA) on the `model`, optionally specifying
5-
`modifications` to the problem. Basically, FBA solves this optimization
6-
problem:
7-
```
8-
max cᵀx
9-
s.t. S x = b
10-
xₗ ≤ x ≤ xᵤ
11-
```
12-
See "Orth, J., Thiele, I. & Palsson, B. What is flux balance analysis?. Nat
13-
Biotechnol 28, 245-248 (2010). https://doi.org/10.1038/nbt.1614" for more
14-
information.
15-
16-
The `optimizer` must be set to a `JuMP`-compatible optimizer, such as
17-
`GLPK.Optimizer` or `Tulip.Optimizer`.
18-
19-
Optionally, you may specify one or more modifications to be applied to the model
20-
before the analysis, such as [`set_objective_sense`](@ref),
21-
[`set_optimizer`](@ref), [`set_optimizer_attribute`](@ref), and
22-
[`silence`](@ref).
23-
24-
Returns a tree with the optimization solution of the same shape as the model
25-
defined by [`fbc_model_constraints`](@ref).
26-
27-
# Example
28-
```
29-
model = load_model("e_coli_core.json")
30-
solution = flux_balance(model, GLPK.optimizer)
31-
```
4+
Make an JuMP model out of `constraints` using [`optimization_model`](@ref)
5+
(most arguments are forwarded there), then apply the `modifications`, optimize
6+
the model, and return either `nothing` if the optimization failed, or `output`
7+
substituted with the solved values (`output` defaults to `constraints`.
8+
9+
For a "nice" version for simpler finding of metabolic model optima, use
10+
[`flux_balance`](@ref).
11+
"""
12+
function optimized_constraints(
13+
constraints::C.ConstraintTreeElem,
14+
args...;
15+
modifications = [],
16+
output = constraints,
17+
kwargs...,
18+
)
19+
om = optimization_model(constraints, args...; kwargs...)
20+
for m in modifications
21+
m(om)
22+
end
23+
J.optimize!(om)
24+
is_solved(om) ? C.constraint_values(output, J.value.(om[:x])) : nothing
25+
end
26+
27+
export optimized_constraints
28+
29+
"""
30+
$(TYPEDSIGNATURES)
31+
32+
Compute an optimal objective-optimizing solution of the given `model`.
33+
34+
Most arguments are forwarded to [`optimized_constraints`](@ref).
35+
36+
Returns a tree with the optimization solution of the same shape as
37+
given by [`fbc_model_constraints`](@ref).
3238
"""
3339
function flux_balance(model::A.AbstractFBCModel, optimizer; kwargs...)
3440
constraints = fbc_model_constraints(model)
Lines changed: 95 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,92 +1,111 @@
1+
12
"""
23
$(TYPEDSIGNATURES)
34
4-
Run parsimonious flux balance analysis (pFBA) on the `model`. In short, pFBA
5-
runs two consecutive optimization problems. The first is traditional FBA:
6-
```
7-
max cᵀx = μ
8-
s.t. S x = b
9-
xₗ ≤ x ≤ xᵤ
10-
```
11-
And the second is a quadratic optimization problem:
12-
```
13-
min Σᵢ xᵢ²
14-
s.t. S x = b
15-
xₗ ≤ x ≤ xᵤ
16-
μ = μ⁰
17-
```
18-
Where the optimal solution of the FBA problem, μ⁰, has been added as an
19-
additional constraint. See "Lewis, Nathan E, Hixson, Kim K, Conrad, Tom M,
20-
Lerman, Joshua A, Charusanti, Pep, Polpitiya, Ashoka D, Adkins, Joshua N,
21-
Schramm, Gunnar, Purvine, Samuel O, Lopez-Ferrer, Daniel, Weitz, Karl K, Eils,
22-
Roland, König, Rainer, Smith, Richard D, Palsson, Bernhard Ø, (2010) Omic data
23-
from evolved E. coli are consistent with computed optimal growth from
24-
genome-scale models. Molecular Systems Biology, 6. 390. doi:
25-
accession:10.1038/msb.2010.47" for more details.
26-
27-
pFBA gets the model optimum by standard FBA (using
28-
[`flux_balance`](@ref) with `optimizer` and `modifications`), then
29-
finds a minimal total flux through the model that still satisfies the (slightly
30-
relaxed) optimum. This is done using a quadratic problem optimizer. If the
31-
original optimizer does not support quadratic optimization, it can be changed
32-
using the callback in `qp_modifications`, which are applied after the FBA. See
33-
the documentation of [`flux_balance`](@ref) for usage examples of
34-
modifications.
35-
36-
The optimum relaxation sequence can be specified in `relax` parameter, it
37-
defaults to multiplicative range of `[1.0, 0.999999, ..., 0.99]` of the original
38-
bound.
39-
40-
Returns an optimized model that contains the pFBA solution (or an unsolved model
41-
if something went wrong).
42-
43-
# Performance
44-
45-
This implementation attempts to save time by executing all pFBA steps on a
46-
single instance of the optimization model problem, trading off possible
47-
flexibility. For slightly less performant but much more flexible use, one can
48-
construct parsimonious models directly using
49-
[`with_parsimonious_objective`](@ref).
50-
51-
# Example
52-
```
53-
model = load_model("e_coli_core.json")
54-
parsimonious_flux_balance(model, biomass, Gurobi.Optimizer) |> values_vec
55-
```
5+
Optimize the system of `constraints` to get the optimal `objective` value. Then
6+
try to find a "parsimonious" solution with the same `objective` value, which
7+
optimizes the `parsimonious_objective` (possibly also switching optimization
8+
sense, optimizer, and adding more modifications).
9+
10+
For efficiency, everything is performed on a single instance of JuMP model.
11+
12+
A simpler version suitable for direct work with metabolic models is available
13+
in [`parsimonious_flux_balance`](@ref).
5614
"""
57-
function parsimonious_flux_balance(
58-
model::C.ConstraintTree,
59-
optimizer;
15+
function parsimonious_optimized_constraints(
16+
constraints::C.ConstraintTreeElem,
17+
args...;
18+
objective::C.Value,
6019
modifications = [],
61-
qp_modifications = [],
62-
relax_bounds = [1.0, 0.999999, 0.99999, 0.9999, 0.999, 0.99],
20+
parsimonious_objective::C.Value,
21+
parsimonious_optimizer = nothing,
22+
parsimonious_sense = J.MIN_SENSE,
23+
parsimonious_modifications = [],
24+
tolerances = [absolute_tolerance_bound(0)],
25+
output = constraints,
26+
kwargs...,
6327
)
64-
# Run FBA
65-
opt_model = flux_balance(model, optimizer; modifications)
66-
J.is_solved(opt_model) || return nothing # FBA failed
6728

68-
# get the objective
69-
Z = J.objective_value(opt_model)
70-
original_objective = J.objective_function(opt_model)
29+
# first solve the optimization problem with the original objective
30+
om = optimization_model(constraints, args...; kwargs...)
31+
for m in modifications
32+
m(om)
33+
end
34+
J.optimize!(om)
35+
is_solved(om) || return nothing
36+
37+
target_objective_value = J.objective_value(om)
7138

72-
# prepare the model for pFBA
73-
for mod in qp_modifications
74-
mod(model, opt_model)
39+
# switch to parsimonizing the solution w.r.t. to the objective value
40+
isnothing(parsimonious_optimizer) || J.set_optimizer(om, parsimonious_optimizer)
41+
for m in parsimonious_modifications
42+
m(om)
7543
end
7644

77-
# add the minimization constraint for total flux
78-
v = opt_model[:x] # fluxes
79-
J.@objective(opt_model, Min, sum(dot(v, v)))
45+
J.@objective(om, J.MIN_SENSE, C.substitute(parsimonious_objective, om[:x]))
8046

81-
for rb in relax_bounds
82-
# lb, ub = objective_bounds(rb)(Z)
83-
J.@constraint(opt_model, pfba_constraint, lb <= original_objective <= ub)
47+
# try all admissible tolerances
48+
for tolerance in tolerances
49+
(lb, ub) = tolerance(target_objective_value)
50+
J.@constraint(
51+
om,
52+
pfba_tolerance_constraint,
53+
lb <= C.substitute(objective, om[:x]) <= ub
54+
)
8455

85-
J.optimize!(opt_model)
86-
J.is_solved(opt_model) && break
56+
J.optimize!(om)
57+
is_solved(om) && return C.constraint_values(output, J.value.(om[:x]))
8758

88-
J.delete(opt_model, pfba_constraint)
89-
J.unregister(opt_model, :pfba_constraint)
59+
J.delete(om, pfba_tolerance_constraint)
60+
J.unregister(om, :pfba_tolerance_constraint)
9061
end
9162

63+
# all tolerances failed
64+
return nothing
9265
end
66+
67+
export parsimonious_optimized_constraints
68+
69+
"""
70+
$(TYPEDSIGNATURES)
71+
72+
Compute a parsimonious flux solution for the given `model`. In short, the
73+
objective value of the parsimonious solution should be the same as the one from
74+
[`flux_balance`](@ref), except the squared sum of reaction fluxes is minimized.
75+
If there are multiple possible fluxes that achieve a given objective value,
76+
parsimonious flux thus represents the "minimum energy" one, thus arguably more
77+
realistic.
78+
79+
Most arguments are forwarded to [`parsimonious_optimized_constraints`](@ref),
80+
with some (objectives) filled in automatically to fit the common processing of
81+
FBC models, and some (`tolerances`) provided with more practical defaults.
82+
83+
Similarly to the [`flux_balance`](@ref), returns a tree with the optimization
84+
solutions of the shape as given by [`fbc_model_constraints`](@ref).
85+
"""
86+
function parsimonious_flux_balance(
87+
model::A.AbstractFBCModel,
88+
optimizer;
89+
tolerances = relative_tolerance_bound.(1 .- [0, 1e-6, 1e-5, 1e-4, 1e-3, 1e-2]),
90+
kwargs...,
91+
)
92+
constraints = fbc_model_constraints(model)
93+
parsimonious_optimized_constraints(
94+
constraints;
95+
optimizer,
96+
objective = constraints.objective.value,
97+
parsimonious_objective = squared_sum_objective(constraints.fluxes),
98+
tolerances,
99+
kwargs...,
100+
)
101+
end
102+
103+
"""
104+
$(TYPEDSIGNATURES)
105+
106+
Pipe-able variant of [`parsimonious_flux_balance`](@ref).
107+
"""
108+
parsimonious_flux_balance(optimizer; kwargs...) =
109+
model -> parsimonious_flux_balance(model, optimizer; kwargs...)
110+
111+
export parsimonious_flux_balance

src/builders/objectives.jl

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,7 @@ $(TYPEDSIGNATURES)
1313
TODO
1414
"""
1515
squared_sum_error_objective(constraints::C.ConstraintTree, target::Dict{Symbol,Float64}) =
16-
C.Constraint(
17-
sum(
18-
(C.value(c) - target[k]) * (C.value(c) - target[k]) for
19-
(k, c) in constraints if haskey(target, k)
20-
),
16+
sum(
17+
(C.squared(C.value(c) - target[k]) for (k, c) in constraints if haskey(target, k)),
18+
init = zero(C.LinearValue),
2119
)

src/solver.jl

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -47,28 +47,3 @@ is_solved(opt_model::J.Model) =
4747
J.termination_status(opt_model) in [J.MOI.OPTIMAL, J.MOI.LOCALLY_SOLVED]
4848

4949
export is_solved
50-
51-
"""
52-
$(TYPEDSIGNATURES)
53-
54-
Make an JuMP model out of `constraints` using [`optimization_model`](@ref)
55-
(most arguments are forwarded there), then apply the modifications, optimize
56-
the model, and return either `nothing` if the optimization failed, or `output`
57-
substituted with the solved values (`output` defaults to `constraints`.
58-
"""
59-
function optimized_constraints(
60-
constraints::C.ConstraintTreeElem,
61-
args...;
62-
modifications = [],
63-
output = constraints,
64-
kwargs...,
65-
)
66-
om = optimization_model(constraints, args...; kwargs...)
67-
for m in modifications
68-
m(om)
69-
end
70-
J.optimize!(om)
71-
is_solved(om) ? C.constraint_values(output, J.value.(om[:x])) : nothing
72-
end
73-
74-
export optimized_constraints

0 commit comments

Comments
 (0)