Skip to content

Commit 7236a7d

Browse files
authored
Merge pull request #810 from LCSB-BioCore/sew-loopless-modification
Add a loopless modification for v2
2 parents 538e1f4 + 4844f47 commit 7236a7d

File tree

9 files changed

+477
-0
lines changed

9 files changed

+477
-0
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
2+
# # Loopless flux balance analysis (ll-FBA)
3+
4+
# Here we wil add loopless constraints to a flux balance model to ensure that
5+
# the resultant solution is thermodynamically consistent. As before, we will use
6+
# the core *E. coli* model, which we can download using
7+
# [`download_model`](@ref):
8+
9+
using COBREXA
10+
11+
download_model(
12+
"http://bigg.ucsd.edu/static/models/e_coli_core.json",
13+
"e_coli_core.json",
14+
"7bedec10576cfe935b19218dc881f3fb14f890a1871448fc19a9b4ee15b448d8",
15+
)
16+
17+
# Additionally to COBREXA and the JSON model format package. We will also need a
18+
# solver that can solve mixed interger linear programs like GLPK.
19+
20+
import JSONFBCModels
21+
import GLPK
22+
import AbstractFBCModels as A
23+
24+
model = load_model("e_coli_core.json")
25+
26+
# ## Running a simple loopless FBA (ll-FBA)
27+
28+
# One can directly use `loopless_flux_balance_analysis` to solve an FBA problem
29+
# based on `model` where loopless constraints are added to all fluxes. This is
30+
# the direct approach.
31+
32+
sol = loopless_flux_balance_analysis(model; optimizer = GLPK.Optimizer)
33+
34+
@test isapprox(sol.objective, 0.8739215069684303, atol = TEST_TOLERANCE) #src
35+
36+
@test all(
37+
v * sol.pseudo_gibbs_free_energy_reaction[k] <= -TEST_TOLERANCE for
38+
(k, v) in sol.fluxes if
39+
haskey(sol.pseudo_gibbs_free_energy_reaction, k) && abs(v) >= 1e-6
40+
) #src
41+
42+
# ## Building your own loopless model
43+
44+
# ConstraintTrees allows one to add loopless constraints to any model. To
45+
# illustrate how one would add loopless constraints to an arbitrary model (and
46+
# not use the convenience function), let's build a loopless model from scratch.
47+
48+
# First, build a normal flux balance model
49+
m = fbc_model_constraints(model)
50+
51+
# Next, find all internal reactions, and their associated indices for use later
52+
internal_reactions = [
53+
(i, Symbol(rid)) for
54+
(i, rid) in enumerate(A.reactions(model)) if !is_boundary(model, rid)
55+
]
56+
internal_reaction_ids = last.(internal_reactions)
57+
internal_reaction_idxs = first.(internal_reactions) # order needs to match the internal reaction ids below
58+
59+
# Construct the stoichiometric nullspace of the internal reactions
60+
import LinearAlgebra: nullspace
61+
62+
internal_reaction_stoichiometry_nullspace_columns =
63+
eachcol(nullspace(Array(A.stoichiometry(model)[:, internal_reaction_idxs])))
64+
65+
# And simply add loopless contraints on the fluxes of the model
66+
m = add_loopless_constraints!(
67+
m,
68+
internal_reaction_ids,
69+
internal_reaction_stoichiometry_nullspace_columns;
70+
fluxes = m.fluxes,
71+
)
72+
73+
# Now the model can be solved as before!
74+
optimized_constraints(m; objective = m.objective.value, optimizer = GLPK.Optimizer)
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# # Community FBA models
2+
3+
using COBREXA
4+
5+
# Here we will construct a community FBA model of two *E. coli* "core" models
6+
# that can interact by exchanging selected metabolites. To do this, we will need
7+
# the model, which we can download if it is not already present.
8+
9+
import Downloads: download
10+
11+
!isfile("e_coli_core.json") &&
12+
download("http://bigg.ucsd.edu/static/models/e_coli_core.json", "e_coli_core.json")
13+
14+
# Additionally to COBREXA and the model format package, we will need a solver
15+
# -- let's use Tulip here:
16+
17+
import JSONFBCModels
18+
import Tulip
19+
import AbstractFBCModels as A
20+
import ConstraintTrees as C
21+
22+
model = load_model("e_coli_core.json")
23+
24+
# Community models work by joining its members together through their exchange
25+
# reactions, weighted by the abundance of each microbe. These exchange reactions
26+
# are then linked to an environmental exchange. For more theoretical details,
27+
# see "Gottstein, et al, 2016, Constraint-based stoichiometric modelling from
28+
# single organisms to microbial communities, Journal of the Royal Society
29+
# Interface".
30+
31+
# ## Building a community of two *E. coli*s
32+
33+
# Here we will construct a simple community of two interacting microbes. To do
34+
# this, we need to import the models. We import the models are ConstraintTrees,
35+
# because it is easier to build the model explicitly than rely on an opaque
36+
# one-shot function.
37+
38+
ecoli1 = fbc_model_constraints(model)
39+
ecoli2 = fbc_model_constraints(model)
40+
41+
# Since the models are joined through their individual exchange reactions to an
42+
# environmental exchange reactionq, we need to identify all possible exchange
43+
# reactions in the community. Since the models are the same, this is
44+
# straightforward here. Additionally, we need to specify the upper and lower
45+
# bounds of these environmental exchange reactions.
46+
lbs, ubs = A.bounds(model)
47+
48+
env_ex_rxns = Dict(
49+
rid => (lbs[i], ubs[i]) for
50+
(i, rid) in enumerate(A.reactions(model)) if startswith(rid, "EX_")
51+
)
52+
53+
# Now we simply create an blank model that only includes environmental exchange reactions.
54+
55+
m = build_community_environment(env_ex_rxns)
56+
57+
# Next we join each member microbe to the model.
58+
m += :bug1^ecoli1
59+
m += :bug2^ecoli2
60+
61+
# We also need to specify the abundances of each member, as this weights the
62+
# flux of each metabolite each member microbe can share with other members or
63+
# the environment.
64+
member_abundances = [(:bug1, 0.2), (:bug2, 0.8)]
65+
66+
m *= :environmental_exchange_balances^link_environmental_exchanges(m, member_abundances)
67+
68+
# Finally, the most sensible community FBA simulation involves assuming the
69+
# growth rate of the models is the same. In this case, we simply set the growth
70+
# rate flux of each member to be the same.
71+
m *=
72+
:equal_growth_rate_constraint^equal_growth_rate_constraints([
73+
(:bug1, m.bug1.fluxes.:BIOMASS_Ecoli_core_w_GAM.value),
74+
(:bug2, m.bug2.fluxes.:BIOMASS_Ecoli_core_w_GAM.value),
75+
])
76+
77+
# Since each growth rate is the same, we can pick any of the growth rates as the
78+
# objective for the simulation.
79+
m *= :objective^C.Constraint(m.bug1.fluxes.:BIOMASS_Ecoli_core_w_GAM.value)
80+
81+
# Since the models are usually used in a mono-culture context, the glucose input
82+
# for each individual member is limited. We need to undo this limitation, and
83+
# rather rely on the constrained environmental exchange reaction (and the bounds
84+
# we set for it earlier).
85+
m.bug1.fluxes.EX_glc__D_e.bound = C.Between(-1000.0, 1000.0)
86+
m.bug2.fluxes.EX_glc__D_e.bound = C.Between(-1000.0, 1000.0)
87+
88+
# We can also be interesting, and limit respiration in one of the members, to
89+
# see what effect this has on the community.
90+
m.bug1.fluxes.CYTBD.bound = C.Between(-10.0, 10.0)
91+
92+
# Finally, we can simulate the system!
93+
sol = optimized_constraints(
94+
m;
95+
objective = m.objective.value,
96+
optimizer = Tulip.Optimizer,
97+
modifications = [set_optimizer_attribute("IPM_IterationsLimit", 1000)],
98+
)
99+
100+
@test isapprox(sol.:objective, 0.66686196344, atol = TEST_TOLERANCE) #src
101+
102+
# At the moment the members cannot really exchange any metabolites. We can
103+
# change this by changing their individual exchange bounds.
104+
mets = [:EX_akg_e, :EX_succ_e, :EX_pyr_e, :EX_acald_e, :EX_fum_e, :EX_mal__L_e]
105+
for met in mets
106+
m.bug1.fluxes[met].bound = C.Between(-1000.0, 1000.0)
107+
m.bug2.fluxes[met].bound = C.Between(-1000.0, 1000.0)
108+
end
109+
110+
sol = optimized_constraints(
111+
m;
112+
objective = m.objective.value,
113+
optimizer = Tulip.Optimizer,
114+
modifications = [set_optimizer_attribute("IPM_IterationsLimit", 1000)],
115+
)
116+
117+
118+
# We can see that by allowing the microbes to share metabolites, the growth rate
119+
# of the system as a whole increased! We can inspect the individual exchanges to
120+
# see which metabolites are being shared (pyruvate in this case).
121+
bug1_ex_fluxes = Dict(k => v for (k, v) in sol.bug1.fluxes if startswith(string(k), "EX_"))
122+
bug2_ex_fluxes = Dict(k => v for (k, v) in sol.bug2.fluxes if startswith(string(k), "EX_"))
123+
124+
#!!! warning "Flux units"
125+
# The unit of the environmental exchange reactions (mmol/gDW_total_biomass/h) is
126+
# different to the unit of the individual species fluxes
127+
# (mmol/gDW_species_biomass/h). This is because the mass balance needs to take
128+
# into account the abundance of each species for the simulation to make sense.
129+
# In this specific case, look at the flux of pyruvate (EX_pyr_e). There is no
130+
# environmental exchange flux, so the two microbes share the metabolite.
131+
# However, `bug1_ex_fluxes[:EX_pyr_e] != bug2_ex_fluxes[:EX_pyr_e]`, but rather
132+
# `abundance_bug1 * bug1_ex_fluxes[:EX_pyr_e] == abundance_bug2 *
133+
# bug2_ex_fluxes[:EX_pyr_e]`. Take care of this when comparing fluxes!
134+
135+
@test isapprox(
136+
abs(0.2 * bug1_ex_fluxes[:EX_pyr_e] + 0.8 * bug2_ex_fluxes[:EX_pyr_e]),
137+
0.0,
138+
atol = TEST_TOLERANCE,
139+
) #src

src/COBREXA.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import AbstractFBCModels as A
2525
import ConstraintTrees as C
2626
import JuMP as J
2727
import SparseArrays: sparse, findnz
28+
import LinearAlgebra: nullspace
2829

2930
include("types.jl")
3031
include("io.jl")
@@ -35,12 +36,15 @@ include("builders/genes.jl")
3536
include("builders/objectives.jl")
3637
include("builders/enzymes.jl")
3738
include("builders/thermodynamic.jl")
39+
include("builders/loopless.jl")
40+
include("builders/communities.jl")
3841

3942
include("analysis/modifications.jl")
4043
include("analysis/flux_balance.jl")
4144
include("analysis/parsimonious_flux_balance.jl")
4245
include("analysis/minimal_metabolic_adjustment.jl")
4346

4447
include("misc/bounds.jl")
48+
include("misc/utils.jl")
4549

4650
end # module COBREXA

src/analysis/loopless_flux_balance.jl

Whitespace-only changes.

src/builders/communities.jl

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"""
2+
$(TYPEDSIGNATURES)
3+
4+
Helper function to create environmental exchange rections.
5+
"""
6+
function environment_exchange_variables(env_ex_rxns = Dict{String,Tuple{Float64,Float64}}())
7+
rids = collect(keys(env_ex_rxns))
8+
lbs_ubs = collect(values(env_ex_rxns))
9+
C.variables(; keys = Symbol.(rids), bounds = lbs_ubs)
10+
end
11+
12+
export environment_exchange_variables
13+
14+
"""
15+
$(TYPEDSIGNATURES)
16+
17+
Helper function to build a "blank" community model with only environmental exchange reactions.
18+
"""
19+
function build_community_environment(env_ex_rxns = Dict{String,Tuple{Float64,Float64}}())
20+
C.ConstraintTree(
21+
:environmental_exchange_reactions => environment_exchange_variables(env_ex_rxns),
22+
)
23+
end
24+
25+
export build_community_environment
26+
27+
"""
28+
$(TYPEDSIGNATURES)
29+
30+
Helper function to link species specific exchange reactions to the environmental
31+
exchange reactions by weighting them with their abundances.
32+
"""
33+
function link_environmental_exchanges(
34+
m::C.ConstraintTree,
35+
member_abundances::Vector{Tuple{Symbol,Float64}};
36+
on = m.:environmental_exchange_reactions,
37+
member_fluxes_id = :fluxes,
38+
)
39+
C.ConstraintTree(
40+
rid => C.Constraint(
41+
value = -rxn.value + sum(
42+
abundance * m[member][member_fluxes_id][rid].value for
43+
(member, abundance) in member_abundances if
44+
haskey(m[member][member_fluxes_id], rid);
45+
init = zero(C.LinearValue),
46+
),
47+
bound = 0.0,
48+
) for (rid, rxn) in on
49+
)
50+
end
51+
52+
export link_environmental_exchanges
53+
54+
"""
55+
$(TYPEDSIGNATURES)
56+
57+
Helper function to set each species growth rate equal to each other.
58+
"""
59+
function equal_growth_rate_constraints(
60+
member_biomasses::Vector{Tuple{Symbol,C.LinearValue}},
61+
)
62+
C.ConstraintTree(
63+
Symbol(bid1, :_, bid2) => C.Constraint(value = bval1 - bval2, bound = 0.0) for
64+
((bid1, bval1), (bid2, bval2)) in
65+
zip(member_biomasses[1:end-1], member_biomasses[2:end])
66+
)
67+
end
68+
69+
export equal_growth_rate_constraints

0 commit comments

Comments
 (0)